Shai-Huludマルウェア攻撃: Tinycolorおよび40超のNPMパッケージが感染
(stepsecurity.io)- NPMエコシステムで人気の @ctrl/tinycolor を含む40超のパッケージに 自己増殖型マルウェア が注入され、開発環境の秘密情報やCI/CD認証情報まで連鎖感染しうるサプライチェーン攻撃が発生。感染バージョンはnpmから削除済み
- 攻撃ペイロードはnpmインストール過程で Webpackバンドル(bundle.js, ~3.6MB) を非同期実行し、環境変数・ファイルシステム・クラウドSDKを通じて 広範な認証情報収集 を行う
- 悪意あるロジックは NpmModule.updatePackage により他のパッケージを強制的にパッチ・配布して カスケード伝播 し、GitHub Actionsに shai-hulud ワークフローを注入して組織のシークレットを toJSON(secrets) で窃取する
- 収集データは 公開GitHubリポジトリ
Shai-Huludの作成によって流出し、通常の開発活動に偽装されるため検知回避性が高い - AWS/GCP/Azure/NPM/GitHub トークンおよびメタデータエンドポイントへのアクセス、TruffleHog ベースのシークレット探索などが秘かに実行される
- パッケージの即時削除・リポジトリの整理・全認証情報の交換とともに、CloudTrail/GCP Auditログの点検、Webhookの遮断、ブランチ保護/Secret Scanning/クールダウンポリシー の導入が求められる
Affected Packages
- 合計195個のパッケージ/バージョン が報告されており、代表的には @ctrl/tinycolor(4.1.1, 4.1.2)、多数の @ctrl/ 名前空間、@crowdstrike/ モジュール群、ngx-bootstrap/ngx-toastr/ng2-file-upload/ngx-color など Angular/Web UIエコシステム 全般、@nativescript-community/ および @nstudio/ モバイルスタック、teselagen/ ライフサイエンスツールチェーン、ember-*、koa2-swagger-ui、pm2-gelf-json、wdio-web-reporter などが含まれる
- 各パッケージごとの 正確なバージョン は原文の表を参照し、該当バージョンを使用しているか 厳密にクロスチェック する必要がある
- 例:
@ctrl/ngx-emoji-mart 9.2.1, 9.2.2,@ctrl/qbittorrent 9.7.1, 9.7.2,ngx-bootstrap 18.1.4, 19.0.3–20.0.5,ng2-file-upload 7.0.2–9.0.1など広範囲
- 例:
Immediate Actions Required
Identify and Remove Compromised Packages
- プロジェクトで 感染パッケージの有無を確認:
npm ls @ctrl/tinycolorなどで点検 - 感染パッケージを 直ちに削除:
npm uninstall @ctrl/tinycolorなどを実行 - 既知の bundle.jsハッシュ検索 でローカル痕跡を点検:
sha256sum | grep 46faab8a...を使用
Clean Infected Repositories
- 悪意あるGitHub Actionsワークフローを削除:
.github/workflows/shai-hulud-workflow.ymlを削除 - リモートに作成された shai-huludブランチを検出・削除:
git ls-remote ... | grep shai-huludの後にgit push origin --delete shai-huludを実行
Rotate All Credentials Immediately
- NPMトークン、GitHub PAT/Actionsシークレット、SSHキー、AWS/GCP/Azure 認証情報、DB接続文字列、サードパーティトークン、CI/CDシークレット などを 全面的に交換 する必要がある
- AWS Secrets Manager/GCP Secret Manager に保存された項目を含め すべてローテーション する必要がある
Audit Cloud Infrastructure for Compromise
- AWS: CloudTrail で
BatchGetSecretValue,ListSecrets,GetSecretValue呼び出しの時点とパターンを点検し、IAM Credential Report で異常なキーの作成・使用を確認 - GCP: Audit Logs で Secret Manager へのアクセス記録を点検し、CreateServiceAccountKey イベントの有無を確認
1件のコメント
Hacker Newsの意見
npmでホストされているパッケージを使う立場として、すべての依存関係とその依存先まで直接監視するのは現実的ではないと感じる。自分はTypeScript/JavaScriptの専門家でもないので、攻撃者が隠した悪意あるコードを簡単には見つけられないと思う。最近考えているのは更新を「遅延モード」にする方法で、つまり最新版ではなく一定期間以上経過したバージョンだけに更新するやり方だ。パッケージが6週間ほど外部にさらされていれば、悪意あるコードが露見している可能性が高いという考え方だ。ただし完全な方法ではないし、セキュリティ問題がある場合には例外的に最新アップデートをすぐ適用できるオプションを持つツールがあればよいと思う
記事でまさに言及されている方法として、NPM Package Cooldown Checkという機能がある。組織で設定した期間内(デフォルトは2日)にリリースされたパッケージバージョンがpull requestに追加されると、自動的にビルドが失敗する。サプライチェーン攻撃の大半は24時間以内に検知されるので、ごく短い待機でもセキュリティ露出を減らせる
すべての依存関係を検査するのは大変なので、できるだけ依存関係の数を減らし、よく知られた信頼できるパッケージだけを使うべきだと言いたい。むしろ、すべての作者を信頼できるほど統制された環境でない限り、ある程度の "not-invented-here" を保つのは常識的な選択だ
package.jsonでバージョンを明示的にpinし、npm ciを使ってpackage-lock.jsonに記載されたバージョンだけをインストールする習慣がある。CIでnpm auditを実行し、パッケージに脆弱性が見つかればアラートを受け取る。こうするとパッケージはほぼ「凍結」状態になり、パッケージの古さ自体によって感染可能性も下がる自分の場合はさらに踏み込んで、バグが実際に自分の利用環境へ影響するときだけ依存関係を更新している。セキュリティ脆弱性があっても影響しないならそのままにする。多くの開発者は不必要に頻繁に依存関係を更新しすぎているが、本当に必要な場合だけでよいと思う。もし更新が頻繁に必要だったり複雑だったりするなら、そのパッケージは最初から使わないか、自分の基準で「凍結」する
Pythonの
uvを使えば似た形で更新を制限できる。たとえばuv lock --exclude-newer $(date --iso -d "2 days ago")のようなコマンドで、2日以内にリリースされたバージョンを除外できる新しいパッケージやバージョンが監視されていないからこうした問題が起こる。Debianのようにセキュリティパッチとバグ修正だけが適用される安定ディストリビューションと、パッケージメンテナが監視する testing/unstable ディストリビューションを分離して運用するのが最善だ。中央集権的なオープンパッケージリポジトリ(NPM、Python、Rustなど)に関わる人たちは皆同じ問題に直面している
開発者文化に問題がある。何百もの(推移的)依存関係を抱えたまま、何も考えず自動更新する文化そのものが問題だ。あれほど大量のサードパーティコードをビルド/実行環境にさらす選択には責任が伴う
ディストリビューション側も、保守しなければならないパッケージ量にますます負担を感じている。実際、こうした理由で言語別エコシステム(例: CPAN、Maven、RubyGemsなど)が発展した。Linuxディストリビューションだけではユーザーが求めるアプリを提供しづらく、freshmeat、linuxbrew、flatpak、PPAなどさまざまな経路が生まれた。どのコミュニティにも、多数の多様なライブラリの複数ブランチを監視し支援できるだけの余力はないと思う
Debian開発者として、upstreamコードを取り込む前に増え続ける「ノイズ」(特に単なるスタイル変更やツール更新)のせいで、実際の変化を見極めるのが非常に難しくなっている。こうした変更は、本当に人の確認が必要なリファクタリング、バグ修正、機能追加、または問題になりそうなコードを探すためのツール結果でない限り、控えてほしい
Rustには cargo vet という仕組みがある。GoogleやMozillaのような企業が参加して、パッケージを共有・検証している
分散性を保ちながらも、ある程度の安全装置を設ける方法はあると思う。たとえば一定規模以上のパッケージは2FA付きの2アカウント承認を必須にする、あるいは人気パッケージは再現可能ビルドシステムからのみnpmへアップロード可能にする、といった案を検討できる。完全な分散を捨てる必要はなく、大規模プロジェクトだけに少し追加の手間を求める程度だ
最近続いているサプライチェーン攻撃のせいで、サーバーレンダリング(JavaScriptなし)をもっと真剣に考えるようになった。HTMXのおかげで、JavaScriptなしでも本当にかなりのところまで行けると気づいた。こうすればアプリはより速く、安定性も高くなる気がする
従来のJS環境は、実際には最も安全なサンドボックス環境だと強調したい。ほぼ30年間、数十億台のデバイスで信頼されないJSコードが実行されてきたが、ブラウザエンジンで大規模攻撃が成功した例は数えるほどしかない。しかしNodeJSとnpmの環境は、セキュリティ面で全面的な再設計が必要な状態だ。leftpadのような出来事は、単なるコードスニペットまでnpmに上げる文化に起因している
こうした攻撃が特定環境(JavaScript)だけの問題へ自動的に矮小化される傾向は奇妙だ。実際にはnpmにすら用意されているセキュリティ強化策が、他の環境(PyPI、Cratesなど)にはまったく適用されていない点のほうが大きな問題に見える
vendoringで露出を減らすことはできるが、根本的な解決にはならないと思う。NPMが本気でセキュリティを考えるなら、公開時の2FAとパッケージ事前スキャンを義務化し、ハードウェアキーでの署名まで強制すべきだ。semverやCRCだけでは不十分で、これらすべてがパッケージ管理システムに標準搭載されるべきだ
実のところ、こうした攻撃はJavaScript固有の問題ではなく、新しい依存関係を追加するときに開発者が十分監視していないことに起因する。これはRustやGoのような他の言語エコシステムにもそのまま当てはまる
パッケージマネージャに大きく依存し、標準ライブラリが貧弱な言語はどれも同じように脆弱だ。長期的にはバニラJavaScriptを使う方向へ戻るべきだと思う。Rustも同様にパッケージ依存度が高い。むしろGoがこの問題では模範的な事例だ
信頼できる鍵でコミット/リリースに署名し、インストールと検証まで行える軽量コードを追跡できるシステムが必要だと思う。すでに sigstoreを活用したnpmのプロビナンス という仕組みはあるが、まだ広く使われておらず、発行者検証程度にとどまっているように見える
2016年にはすでにNPMへ この脆弱性が報告されていた(CERT勧告)。しかしNPMの回答はWAI(working as intended)だった
WAIが何の略か分からない人のために言うと、一般的には “working as intended” の略だ
たとえ postinstall スクリプトがまったくなくても、ビルド過程、サーバー起動、テストなどでモジュールをimportすれば結局悪意あるコードは実行される気がする。結局 npm install のあとで何かを実際に動かす瞬間は必ずあるので……
left-pad騒動のときにここで見たコメントを思い出す。著名なnpmメンテナが600個のnpmパッケージと1,200行のJavaScriptコードを持っているという話だった。手本として挙げたいケースは esbuild で、外部依存はほとんどなく、Go標準ライブラリだけを使っている。
「次世代」と呼ばれる他のプロジェクトも依存チェーンを見ると、biomejs、swc ともかなり少ない。ただしRustの元コードを見ると、結局 biomejs、swc も多くの依存関係を持っている。こうしたプロジェクトが広がれば、cargoエコシステムも同じ轍を踏むだろうと思う。もしesbuildのような厳格なスタイルで書かれた大規模プロジェクトを知っている人がいれば、ぜひ勧めてほしい
Goへ移った理由の一つは、purego方式のライブラリの流行だ。たいてい標準ライブラリと golang.org/x にしか依存せず、CGOなしでコンパイルできるので可搬性が高い。
go mod vendorで短期的なリスク管理はできるが、根本的な解決ではない。Goもまたパッケージ検証(署名/鍵チェックなど)をエンドツーエンドで提供していないので、結局脆弱性は残る。特にCI/CDインフラへ多くが集中しているが、署名鍵を渡さずにビルド・配布できるなら安全性も高められるはずだ。パッケージマネージャはGPG署名を推奨し、gitコミットにも署名を導入して配布すべきだと思うeslintのケースは、代表的にうんざりさせられる例だ。依存関係グラフ を見ると膨大で、メンテナが依存削減を優先しないなら、結局ほかの解決策(oxlint)へ乗り換えるしかない
簡単な機能は自分で作って外部依存を減らすのが答えだ。ふつう、そうするだけでも全依存関係の3分の2は削減できる。特にleft-padのような単純なものは自分で実装し、小さなユニットとテストで手元に置いておけば、管理負担もそれほど大きくない。不要な依存関係は思い切って排除すべきだ
Rustプロジェクトの root
Cargo.tomlに書かれているのはワークスペース全体向けで、実際の各crate(パッケージ)の依存関係はもっと浅い。さらに深く見ないと実際の依存構造は分からない欠点は、JavaScriptプロジェクトを点検するために今やGolangまで読まなければならないことだ。しかも post-install でまた
node install.jsが実行されるので、結局は完全に信頼するかコードを全部読むしかないnpmが今でもすべての依存関係の postinstall スクリプトをデフォルトで実行するのが信じられない。PnpmやBunは許可リストに登録された場合だけ実行し、Composerはそもそも依存関係に対してライフサイクルスクリプトを実行しない。ビルドや開発環境で依存パッケージが持つリスクを考えると、この方式のほうが安全だと思う
こうした大規模攻撃が他のパッケージマネージャ(例: Rust
build.rs、Python、Javaなど)ではあまり頻繁に聞かれない理由が気になる。postinstallはもちろん、実際にはほぼすべてのエコシステムで原理的には可能なのに、npmにばかり事件が集中しているように見えるPnpmのデフォルトが スクリプト遮断に変わった のを見た。コミュニティの反応(スクリプト許可をめぐる使用感、allow コマンドの乱用など)が気になるし、Pythonパッケージングコミュニティでも wheel variants に関連した似た議論が進んでいる。他エコシステムの経験を参考にしたい
今回の攻撃は180個以上のパッケージに拡大した。Aikido Securityブログ 参照
この攻撃を最初に発見したのが誰なのか気になる。ブログごとに功績の書き方が違っていて興味深い。Aikidoは「我々が大規模攻撃を発見した」と言い、Socket、Ox、Safety、Phoenix、Semgrepなどもそれぞれ違う書き方をしている
私はAikidoで働くMackenzieだ。最初にこの件を報告したのは開発者のDaniel Pereiraで、この人がSocketへ伝え、Socketが最初の40パッケージとマルウェアを分析した。その後Aikidoが追加で147パッケージとCrowdstrikeパッケージまで発見した。実際にはStepが最初に、このマルウェアが自己伝播型ワームであることを見抜いた。複数の組織が独立してさまざまな役割を果たした点が面白い
複数の開発者がほぼ同じ時期に発見したようで、StepとSocketはそれぞれ別の人に言及している。最終的には、業界のセキュリティベンダーがAIコード解析(Socket、Aikido)やeBPFパイプライン監視(Step)など、それぞれの方法で検出した
これほど多くのベンダーが独立に検知できたのなら、その技術をnpmへ直接共有して、悪意あるパッケージ登録そのものを防げなかったのかという疑問が湧く。そうすると早期警告システムをベンダーが販売できなくなるので、出さないのかもしれない
OPの記事は「@franky47がこの現象を発見し、すぐコミュニティへ GitHub issue で知らせた」という文を直接引用している
攻撃者が付けた名前「Shai Hulud」はかなり洒落ていると思う。巨大な虫の名前を実際のワーム型マルウェアに付けているし、中核の
bundle.jsも3.6MBと巨大だ。亜種のマルウェアですらnpmらしく非常に大きくなってしまったそのうち、あるサプライチェーン攻撃が別のサプライチェーン攻撃を偶然呼び込むようなことも起きそうな気がする
マルウェアもまたムーアの法則に従う。1991年の tequila virus は2.6KBだったのに、今では数MBだ