Shai-Huludは、npmパッケージに感染する自己増殖型のワームです。被害者を次の拡散の担い手に変えるソフトウェアサプライチェーン攻撃であり、パッケージのインストール時に実行され、マシンからnpmトークン、GitHubトークン、クラウドの鍵を盗み、そのnpmトークンを使って被害者が管理する他のパッケージの感染バージョンを公開します。2025年9月の最初のウェーブ以降、繰り返し再来しており、2026年5月には作成者がソースコードを公開したため、それ以降のウェーブはそれぞれ別のグループによるものである可能性があります。自社のビルドのどれかが感染バージョンをインストールしていた場合は、そのジョブは侵害されたものとして扱い、ジョブが読み取れたすべてのシークレットをローテーションしてください。
被害の大半はCIで発生しています。2025年11月のウェーブについて、Wizの調査では、開発者のマシン上の感染はわずか23%で、残りは主にCI/CDパイプライン上にあり、最も多かったのはGitHub Actionsでした。ビルドは数百のパッケージをインストールし、公開やデプロイに必要なシークレットを保持し、誰にも監視されないまますべてのインストールスクリプトを実行します。そのため新しいウェーブが報じられるたびに、どのチームも同じ問いを抱きます。自社のビルドのどれかが感染バージョンをインストールしたのか、という問いです。
何が起きたか
| 時期 | 名称 | 何が異なったか |
|---|---|---|
| 2025年9月 | Shai-Hulud | postinstallスクリプトが3.6 MBのbundle.jsを実行し、TruffleHogでシークレットを探し、メンテナーのパッケージを最大20個再公開しました。@ctrl/tinycolorや18個の@crowdstrikeパッケージを含む500以上のパッケージが影響を受けました。CISAは9月23日にアラートを公開しました。 |
| 2025年11月 | Sha1-Hulud: The Second Coming | preinstallに移行し、Bunランタイムをインストールして10 MBのペイロードを実行し、メンテナー1人あたり最大100個のパッケージにバックドアを仕込み、感染したマシンをセルフホストのGitHubランナーとして登録しました。Zapier、ENS、PostHog、Postmanのパッケージが被害を受け、Wizはシークレットが漏えいしたリポジトリを25,000以上確認しました。 |
| 2025年12月 | 「3.0」テスト | 改変版が1つのパッケージに含まれていましたが、目立った拡散はありませんでした。 |
| 2026年5月 | TanStack | 42個の@tanstackパッケージの悪意あるバージョン84件。攻撃者はnpmトークンを盗んでいません。pull_request_targetワークフロー、汚染されたActionsキャッシュ、ランナーのメモリから読み取ったOIDCトークンを使い、TanStack自身のtrusted publishingを通じて公開しました。CVE-2026-45321として追跡され、5月27日にCISAのKEVカタログに追加されました。 |
| 2026年5月 | AntV | 323パッケージの639バージョン。GitHub ActionsのLinuxランナー上では、ペイロードがランナープロセスのメモリからシークレットを読み取り、ログのマスキングを回避しました。GitHubは61,274件のnpm書き込みトークンを無効化しました。 |
| 2026年6月 | Miasma | 32個の@redhat-cloud-servicesパッケージが、注入されたワークフローを通じて有効な来歴付きで公開されました。感染ごとに個別に暗号化されたペイロードを含んでいたため、ハッシュベースの検出は機能しませんでした。 |
| 2026年8月 | CHAINDROP | keyv、cacheable、cache-managerなど人気のキャッシュ関連パッケージで、合計数百のパッケージが影響を受けました。リポジトリ内のエディターやAIアシスタントの設定ファイルを通じて永続化しました。 |
| 2026年9月 | 再出現 | 9月7日、npmが新たに導入した公開時のマルウェアスキャンをすり抜けた4つのパッケージで、ペイロードが再び現れました。 |
| 2026年10月 | tensorlake | 10月8日、ワームを含む[email protected]が公開されました。OX Securityは新しい攻撃者の鍵を確認し、「独立した脅威アクター、またはShai-Huludの名前を使う新しいグループ」の可能性を示唆しています。 |
CIジョブ内でワームが行うこと
詳細はウェーブごとに変わりますが、パターンは共通しています。
- インストール時に実行される。
preinstallまたはpostinstallスクリプトは、npm installまたはnpm ciが感染バージョンに到達した時点で実行されます。自社のコードがそれをインポートする必要はありません。 - シークレットを収集する。 環境変数、
.npmrcのトークン、メタデータサービスやシークレットマネージャーから得たクラウドの認証情報、さらにTruffleHogがディスク上で見つけたあらゆるものです。後のウェーブではGitHub Actionsランナーのメモリを直接読み取り、ステップに一度も渡されていないシークレットや、ログではマスキングで隠されるはずのシークレットまで取得しました。 - 外部に持ち出す。 初期のウェーブでは、ワームにちなんだ名前の公開GitHubリポジトリを作成し、リポジトリのすべてのシークレットを
webhook.siteに送信するワークフローをプッシュしました。後のウェーブでは、攻撃者のサーバーやメッセージングネットワークを使いました。 - 拡散し、居座る。 盗んだnpmトークンで他のパッケージの感染バージョンを公開します。GitHubトークンを使って、ワークフローを追加したり、セルフホストランナーを登録したり、次に誰かがリポジトリを開いたときに実行される設定ファイルを仕込んだりします。
- CIでは振る舞いを変える。 2025年11月のペイロードは
GITHUB_ACTIONS、BUILDKITE、CIRCLE_SHA1などの変数を確認し、CI上では同期的に実行して、ビルドが先に進む前に処理を終えていました。
影響を受ける対象
感染バージョンがレジストリ上にあった間に、それをインストールしたマシンやビルドを持つすべての人です。開発者のノートPC、CIジョブ、npm installを実行するDockerビルド、セルフホストランナーが含まれます。パッケージに直接依存している必要はなく、推移的依存関係やnpxで実行したツールでも十分です。ウェーブごとにパッケージとバージョンのリストがあり、感染バージョンは通常数時間から数日で削除されるため、問題はその期間中に何かがそれをインストールしたかどうかです。
仕組み
Shai-Huludはnpmのバグではありません。本来は正当なパッケージの中に公開された悪意あるコードであり、感染したパッケージに対するGitHubのマルウェアのアドバイザリは、これを埋め込まれた悪意あるコード(CWE-506)に分類しています。各ウェーブはいくつかの手法を組み合わせています。
- インストール時の実行。 npmは、インストール中にパッケージのライフサイクルスクリプト(
preinstall、postinstall、prepareなど)を、ユーザーまたはCIジョブの権限で実行します。感染バージョンは依存関係ツリーにあるだけでよく、何かがそれをインポートする必要はありません。 - 盗んだ公開権限による拡散。 被害者から奪ったnpmトークンやGitHubトークンを使い、ワームはその被害者の他のパッケージにペイロードを加えた新しいバージョンを公開します。そのため、感染したメンテナーが一人増えるたびに、さらに多くのパッケージが感染します。
- 被害者自身のリリースパイプラインを通じた公開(TanStack、2026年5月)。 GitHubのアドバイザリによれば、攻撃者は
pull_request_targetワークフロー、フォークとベースの信頼境界をまたいで汚染されたGitHub Actionsのキャッシュ、ランナーのメモリから読み取ったOIDCトークンを連鎖させ、公開用のワークフローを変更することなく、TanStackのtrusted publishingを通じて公開しました。感染バージョンはフォーク内のコミットへのオプションの依存関係を宣言しており、そのprepareスクリプトがペイロードを実行したあと意図的に失敗したため、npmはそれを破棄し、node_modulesに痕跡は残りませんでした。 - 有効な来歴(Miasma、2026年6月)。 注入されたワークフローが被害者自身のリポジトリから公開したため、感染バージョンは有効な来歴を備えていました。また、感染ごとに固有に暗号化されたペイロードによって、ハッシュによる検出は機能しませんでした。
影響を受けているかの確認方法
- 該当ウェーブの影響を受けるバージョンのリストを入手する。 セキュリティベンダーは数時間以内にリストを公開します。たとえばDatadogは、パッケージとバージョンのリストを含む侵害指標(IoC)のリポジトリを管理しています。
- すべてのロックファイルを検索する。 デフォルトブランチだけでなく、該当期間中にCIを実行したすべてのブランチが対象です。
- ビルドそのものを検索する。 CIログで
setup_bun.js、bun_environment.jsなどアドバイザリに記載されたファイル名や、影響を受けるバージョンに言及したインストール出力を探します。ビルドごとにSBOMを保管している場合は、その中からバージョンを検索します。 - GitHub組織を確認する。 ワームにちなんだ名前の新しいリポジトリ、自分たちが書いていないワークフローファイル(たとえば
shai-hulud-workflow.yml、discussion.yaml、formatter_*.yml)、登録した覚えのないセルフホストランナーがないかを確認します。
調整して使えるコマンドです。すべてのリモートブランチのロックファイルを検索するには、該当ウェーブの影響を受けるバージョンをaffected.txtに1行に1つずつname@versionの形式で書きます。
git fetch --all --quiet
for ref in $(git for-each-ref --format='%(refname:short)' refs/remotes); do
while read -r pkg; do
name=${pkg%@*}; version=${pkg##*@}
git grep -n -A3 -F "$name" "$ref" -- '*package-lock.json' '*yarn.lock' '*pnpm-lock.yaml' \
| grep -F "$version" | sed "s|^|$pkg |"
done < affected.txt
done
組織のセルフホストランナー、ワームに言及する新しいリポジトリ、ワームが作成することがわかっているワークフローファイルを一覧表示するには、次のとおりです(ORGを置き換えてください)。
gh api --paginate orgs/ORG/actions/runners --jq '.runners[] | [.name, .os, .status] | @tsv'
gh repo list ORG --limit 1000 --json name,description,createdAt \
--jq '.[] | select((.description // "") | test("hulud"; "i")) | [.name, .createdAt] | @tsv'
for repo in $(gh repo list ORG --limit 1000 --json name --jq '.[].name'); do
gh api "repos/ORG/$repo/contents/.github/workflows" --jq '.[].name' 2>/dev/null \
| grep -E '^(shai-hulud-workflow\.yml|discussion\.yaml|formatter_.*\.yml)$' \
| sed "s|^|$repo |"
done
リポジトリが独自のランナーを持つこともあります(gh api repos/ORG/REPO/actions/runners)。また初期のウェーブでは、トークンを盗まれた開発者の個人アカウントにリポジトリが作成されたため、開発者にも自分のアカウントを確認するよう依頼してください。
ロックファイルだけでは判断できない理由
標準的なアドバイスは、package-lock.json、yarn.lock、pnpm-lock.yamlから影響を受けるバージョンを検索することです。まずはそれを行ってください。ただし、ロックファイルがクリーンでも、ビルドがクリーンだったことの証明にはなりません。
- ロックファイル外でインストールされたパッケージ。
npx some-toolは、プロジェクトの依存関係ではないパッケージをnpmキャッシュにインストールします。セットアップステップでのnpm install -g、ビルドスクリプト内でインストールされるツール、Dockerfile内のnpm installは、いずれもプロジェクトのロックファイルを迂回します。 - 今日ブランチにあるロックファイルは、実行時のロックファイルではない。 3週間前のビルドは、そのビルドをトリガーしたブランチで、当時のロックファイルの内容を使っています。依存関係を更新するプルリクエストは、誰かがレビューする前に新しいバージョンでCIを実行します。
- キャッシュは以前にインストールされたものを復元する。 復元された
node_modulesやnpmキャッシュには、現在のロックファイルにはもう記載されていないバージョンが含まれている可能性があります。 npm ciはロックファイルの記載どおりにインストールする。 感染バージョンがすでにロックされていれば、厳格なインストールはそれを忠実にインストールします。- 正しく公開されたバージョンでも悪意を含みうる。 TanStackとMiasmaは自身のリリースワークフローを通じて侵害され、Miasmaの感染バージョンは有効な来歴を備えていました。バージョンが正しく公開されたことを確認しても、それがクリーンであることはわかりません。
本当に必要なのは、感染バージョンがレジストリ上にあった期間全体について、すべてのリポジトリとすべてのブランチで、各ビルドがどのパッケージバージョンを取得したかの記録です。影響はあるか?新しいCVEをビルド全体で評価する方法をご覧ください。
修正方法と、まだパッチを適用できない場合の対処
- 影響を受けるバージョンをインストールしたビルドがあれば、そのジョブは侵害されたものとして扱う。 ジョブが読み取れたすべてのシークレット(npmトークン、GitHubトークン、クラウドの認証情報、デプロイキー)をローテーションします。後のウェーブでは盗んだシークレットを他の被害者のアカウントで公開していたため、自社のアカウントに何も現れていないからといって安全だと考えないでください。
- キャッシュを消去する。 GitHub Actionsのキャッシュと、開発者のマシンやセルフホストランナー上のnpmキャッシュを削除し、次のビルドがクリーンなバージョンをインストールするようにします。
gh cache delete --all --repo ORG/REPO # GitHub Actions caches, per repository
npm cache clean --force
rm -rf node_modules "$(pnpm store path)" # pnpm; for Yarn: yarn cache clean
- クリーンなバージョンに固定する。 そのうえで、ワームが追加したワークフロー、リポジトリ、ランナー、設定ファイルを削除します。
クリーンアップの検証
- すべてのロックファイルがクリーンなバージョンに解決される。 各ブランチで
npm ls <package>(またはpnpm whyやyarn why)がインストールされたバージョンを示します。どれも該当ウェーブのリストに含まれていてはいけません。 - クリーンインストールにペイロードファイルが含まれない。 上記のキャッシュ削除のあとで再度インストールし、アドバイザリに記載されたペイロードファイルを探します。
find node_modules -name bundle.js -size +3M \
-o -name setup_bun.js -o -name bun_environment.js -o -name router_init.js
- 古い認証情報がもう使えない。 失効させたnpmトークンが
npm token listから消えており、古いGitHubトークンとデプロイキーが削除され、クラウドの鍵は置き換えるだけでなく無効化されています。 - キャッシュが消えている。 影響を受けたどのリポジトリでも、
gh cache listにクリーンアップ前に作成されたキャッシュが表示されません。 - ワームが追加したものが何も残っていない。 自分たちが作成していないリポジトリ、ワークフローファイル、セルフホストランナー、エディターやAIアシスタントの設定ファイルがありません。
- 次のビルドがクリーンである。 ロックファイルだけでなく、そのビルドのログやビルドごとのSBOMから、実際にインストールされたバージョンを確認します。
次回の被害を減らすには
- 可能な限りインストールスクリプトを無効にする。 2026年7月にリリースされたnpm v12では、インストールスクリプトがデフォルトで無効になり、パッケージごとに許可できます。pnpmとYarnにも同様の制御があります。
- 新しいバージョンの採用を待つ。 感染バージョンの多くは、数時間から数日以内に発見され削除されます。依存関係の更新にクールダウン期間を設ければ(DependabotとRenovateの両方が対応しています)、そうしたバージョンをビルドに取り込まずに済みます。2026年10月20日からGitHub Enterprise Cloud向けにパブリックプレビューとして提供されているGitHubのパッケージファイアウォールも、npmのインストールに悪意あるパッケージが含まれていないかを確認し、クールダウンも提供しています。
- trusted publishingと短命なトークンで公開する。 npmは2025年12月にすべてのクラシックトークンを失効させました。長期間有効なトークンではなく、CIからのOIDCベースのtrusted publishingを使い、公開用のワークフローはビルドやテストのワークフローと分けてください。
- ワームが悪用するワークフローを堅牢化する。
pull_request_targetを避け、アクションをコミットSHAで固定し、GITHUB_TOKENには各ジョブに必要な権限だけを与えます。GitHub Actionsのシークレット:漏えいの経路と防ぎ方をご覧ください。 - ビルドの接続先を制限する。 ビルドに必要なのは、パッケージレジストリといくつかのAPIだけです。接続先をそれらに限定すれば、
webhook.siteや攻撃者のサーバーへのワームの通信は失敗します。エグレスポリシーでGitHub Actionsからのデータ持ち出しを防ぐ方法をご覧ください。ただし、GitHub自身のAPIなど、ビルドが正当に利用するホストを経由した持ち出しは防げません。一部のウェーブはこれを利用していました。
侵害の確認
Shai-Huludは設計上悪意を持っています。感染バージョンをインストールしたマシンやジョブは、すべてそれを実行しています。侵害指標はウェーブごとに異なるため、確認するウェーブのアドバイザリを使ってください。一次情報による指標は次のとおりです。
| ウェーブ | 侵害指標 | 出典 |
|---|---|---|
| 2025年9月 | ペイロードbundle.js、SHA-256 46faab8ab153fae6e80e7cca38eab363075bb524edd79e42269217a083628f09。ワークフロー.github/workflows/shai-hulud-workflow.yml(ブランチshai-hulud上)。Shai-Huludという名前の公開リポジトリ。webhook.siteへの接続 | StepSecurity、CISA |
| 2025年11月 | setup_bun.js、SHA-1 d1829b4708126dcc7bea7437c04d1f10eacd4a16。bun_environment.js、SHA-1 d60ec97eea19fffb4809bc35b91033b52490ca11または3d7570d14d34b0ba137d502f042b27b0f37a59fa。cloud.json、contents.json、environment.json、truffleSecrets.json、actionsSecrets.jsonを含むリポジトリ。ワークフロー.github/workflows/discussion.yamlとformatter_*.yml。SHA1HULUDという名前のセルフホストランナー | Wiz |
| 2026年5月、TanStack | "@tanstack/setup": "github:tanstack/router#79ac49eedf774dd4b0cfa308722bc463cfe5885c"(optionalDependencies内)。パッケージのルートにあるペイロードrouter_init.js(約2.3 MB)。ヘルパーtanstack_runner.js。filev2.getsession.orgとseed1.getsession.org~seed3.getsession.orgへの接続、litter.catbox.moeからの第2段階のダウンロード | GHSA-g7cv-rxg3-hmpx |
後のウェーブでは感染ごとにペイロードが変わったため、ファイルのハッシュが一致しなくても何の証明にもなりません。まずパッケージのバージョンで照合してください。
侵害を前提とすべきとき。 影響を受けるバージョンをインストールしたホストとCIジョブは、インストールが失敗した場合やnode_modulesに何も残っていない場合でも、すべて侵害されたものとして扱ってください。TanStackに関するGitHubのアドバイザリは、影響を受けるバージョンをインストールした開発環境やCI環境は「侵害されたものとみなすべきです」としています。ウェーブが報じられた日からではなく、感染バージョンが最初に公開された時点から確認してください。
認証情報をローテーションする。 インストールが到達できたものはすべてローテーションします。ポストモーテムによれば、TanStackのウェーブでは、AWS、GCP、Kubernetes、Vault、GitHub、npm、SSHの認証情報が対象でした。インストール期間中とその後の、影響を受けたホストからの活動をクラウドの監査ログで確認してください。
キャッシュと成果物を削除する。 CISAの勧告どおり、アーティファクトリポジトリや依存関係プロキシから影響を受けるバージョンのキャッシュされたコピーを探して削除します。node_modules、npmキャッシュ、CIのキャッシュを削除してください。TanStackのウェーブでは攻撃者の侵入口が汚染されたpnpmストアのキャッシュだったため、パッケージのキャッシュだけでなくActionsのキャッシュも確認してください。侵害されたジョブが生成したものはすべて再ビルドして再リリースします。
顧客への伝え方
npmを使ってビルドしたソフトウェアを出荷しているなら、製品にワームが含まれていないか、リリースパイプラインが侵害されていないかを顧客から尋ねられます。何を確認し、何を行ったかで回答してください。
[日付]から[日付]までの[製品]のすべてのビルドを、公開されているShai-Huludに感染したnpmパッケージバージョンのリストと照合しました。[影響を受けるバージョンをインストールしたビルドはありませんでした。] / [[日付]の[コンポーネント]のビルドが[package@version]をインストールしていました。それらのビルドがアクセスできた認証情報をローテーションし、クリーンな依存関係から[製品]を再ビルドして[バージョン]をリリースしました。リリース[x~y]が影響を受けます。[バージョン]にアップグレードしてください。] 新しいウェーブで状況が変われば、このお知らせを更新します。
まだ確認中であれば、そう伝え、いつ回答するかを伝えてください。第一報と各結果のテンプレートは、サプライチェーン攻撃後に顧客へどう回答するかをご覧ください。
CRACIの役割
CRACIはGitHub Actionsのジョブを自社のランナー上で実行し(runs-on: craci)、各ジョブがnpmやその他のレジストリから実際に取得したすべてのパッケージを、キャッシュから復元されたパッケージも含めて記録します。この記録は、前述の死角をカバーします。記録はロックファイルの宣言ではなくジョブが実際にダウンロードしたものに基づくため、npxのツール、グローバルインストール、ビルドスクリプト内でのインストールもすべて記録に現れます。新しいウェーブが報じられたら、ロックファイルやログから再構成する代わりに、CRACIが記録したビルドの中から影響を受けるバージョンを探せます。各ジョブのネットワークトレースで接続先もわかります。ビルド履歴は過去のすべてのビルドをSBOMとともに保持するため、保持期間内であれば、どのビルドに感染バージョンが含まれていたか、露出していた期間がいつだったかがわかります。
CRACIのエグレスポリシーはジョブの接続先を制限し、ジョブ開始前に検証され、フェイルクローズで動作し、ポリシーで許可されていない宛先にジョブが接続しようとするとアラートを送信します。TanStackのウェーブのようにCVEが公開されている感染リリースは他の脆弱性と同様にフラグが立ち、ポリシーゲートでそれを含むビルドをブロックできます。CRACIはパッケージの悪意ある振る舞いをスコアリングせず、アドバイザリが出る前の新しい感染バージョンにフラグを立てることもありません。感染バージョンをインストール前にブロックしたい場合は、パッケージレピュテーションツールと組み合わせてください。
その他のインシデント:すべての脆弱性。
出典と更新履歴
一次情報と独自の調査:
- CISA:2025年9月23日のアラート、KEVカタログ
- GitHub:GHSA-g7cv-rxg3-hmpx(CVE-2026-45321)、より安全なnpmサプライチェーンに向けた計画、npmクラシックトークンの失効
- TanStackのポストモーテム
- 2025年11月のウェーブに関するWizの記事とその後の影響
- AntVに関するMicrosoftの記事とMicrosoftの検出ガイダンス
- DatadogとElasticによるCHAINDROPの記事
- 2025年9月のウェーブに関するStepSecurityの記事
- FIRST:CVE-2026-45321のEPSSスコア
更新履歴:
- 2026-11-11:ボックスにEPSS、CWEの分類、クレジットを追加し、コピーして使える確認コマンドと、ワームの仕組み、クリーンアップの検証、ウェーブごとの侵害指標を含む侵害の確認の各節を追加。
- 2026-11-11:ブログから脆弱性セクションに移動。2026年10月8日のtensorlakeのウェーブ、2026年9月の再出現の日付、TanStackのウェーブのCVE-2026-45321とそのKEV掲載、GitHubのパッケージファイアウォールを追加。
- 2026-09-25:CRACIブログで初版公開。