npm install を実行しただけで、あなたのAWSキー・GitHubトークン・npmクレデンシャルが全部盗まれる。そして「すぐrevokeしろ」と誰もが言うが、その順番を間違えると二次被害が発生する。
2026年8月4日、史上最大規模のnpmサプライチェーン攻撃「Shai-Hulud」が発生した。ESLintを使っているプロジェクトは、何もしていなくてもほぼ全員が被害範囲に入っている。
何が起きたか
攻撃者は keyv や flat-cache など超人気npmパッケージのメンテナ Jared Wray のGitHubアカウントを乗っ取り、悪意あるバージョンをnpmに公開した。
侵害が確認された主要パッケージは次の通りだ。
| パッケージ | 侵害バージョン | 週次ダウンロード数 |
|---|---|---|
| keyv | 6.0.0 | 約1.27億 |
| flat-cache | 6.1.24 | 約1.50億 |
| file-entry-cache | 11.1.6 / 11.1.7 | 約1.48億 |
| cacheable-request | 13.0.20 | 約1.37億 |
| cacheable | 2.5.1 | 約3,000万 |
| cache-manager | 7.2.10 | 約1,600万 |
最終的に 868パッケージ・1,381バージョン に感染が拡大し、合計月間インストール数は 20億を超えた。
なぜここまで広がったか。ESLintの依存ツリーを見ると一目瞭然だ。
ESLint
└─ file-entry-cache(侵害: 11.1.6/11.1.7)
└─ flat-cache(侵害: 6.1.24)
└─ keyv(侵害: 6.0.0)
ESLintを使っているプロジェクトなら、誰もこれらを直接インストールしていなくても全員がblast radius内に入る。
そしてもう一つの落とし穴がある。CVEが存在しない。npm audit は何も検出しない。侵害バージョンはnpmの脆弱性データベースに登録されていないため、通常のセキュリティスキャンをすり抜ける。
攻撃の仕組み
攻撃の起点は package.json に仕込まれた1行だ。
"scripts": {
"preinstall": "node setup.mjs"
}
preinstall フックはインストールが完了する前に必ず実行される。途中でエラーが出ても関係ない。npm install を実行した瞬間、悪意あるコードが走る。
実行フローはこうなっている。
Bunランタイムを経由する理由が巧妙だ。ダウンロード先は github.com の公式リリースページだ。ネットワークログには信頼できるドメインへの通信しか残らない。さらにBunはNode.jsとは別プロセスとして起動するため、Node.jsを監視するEDRルールを回避できる。実行後にバイナリを削除するので、フォレンジック調査でもアーティファクトがほぼ残らない。
Math_Symbol.jsが標的にするファイルは約140パターンに及ぶ。npm・GitHub CLI・AWS・Azure・GCP・Kubernetes・HashiCorp Vault・SSH鍵まで網羅している。さらに .claude/credentials.json や .cursor/credentials.json といったAI開発ツールの認証情報も標的だ。
ワームとして自己増殖する
クレデンシャルを盗むだけなら典型的なインフォスティーラーだが、このマルウェアは一歩先を行く。盗んだnpmトークンを使って、自分自身を他のパッケージに書き込むワームとして動作する。
活発なフェーズでは数分ごとに50〜100パッケージが新規感染していた。これが868パッケージ・1,381バージョンという規模になった理由だ。
自分が被害範囲内か確認する
まず lockfile を確認する。以下のコマンドで侵害バージョンが依存ツリーに含まれているかチェックできる。
# npm / pnpm / yarn 全対応
grep -rn \
"keyv@6\.0\.0\|flat-cache.*6\.1\.24\|file-entry-cache.*11\.1\.[67]" \
package-lock.json pnpm-lock.yaml yarn.lock 2>/dev/null
# インストール済みの依存ツリーを確認
npm ls keyv flat-cache file-entry-cache cacheable cacheable-request cache-manager
感染済みファイルがローカルに残っていないかも確認する。
find . -name "Math_Symbol.js" -o -name "setup.mjs" 2>/dev/null | grep -v node_modules/.bin
# IDEパーシスタンスの確認(後述)
find . -path "*/.vscode/tasks.json" -newer package.json 2>/dev/null
find . -path "*/.claude/setup.mjs" 2>/dev/null
lockfile に侵害バージョンが含まれていた場合、次のセクションを必ず読んでから対応を始めること。
⚠️ revokeより先にやること
ここが今回の記事で最も伝えたい点だ。
「侵害を検知したらすぐトークンをrevokeせよ」というのが一般的なインシデントレスポンスの常識だが、このマルウェアにはそれが通用しない。GitHubトークンの失効を検知すると発動するデッドマンズスイッチが仕掛けられている。
デッドマンズスイッチは ~/.local/bin/gh-token-monitor.sh などのスクリプトとして常駐する。トークンが失効すると追加ペイロードを実行する設計だ。revokeが凶器になる、という逆説だ。
まずデッドマンズスイッチを除去してから、トークンをrevokeする。
# デッドマンズスイッチの除去
rm -f ~/.local/bin/gh-token-monitor.sh
rm -rf ~/.config/gh-token-monitor/
# macOS の LaunchAgent
rm -f ~/Library/LaunchAgents/com.user.gh-token-monitor.plist
launchctl unload ~/Library/LaunchAgents/com.user.gh-token-monitor.plist 2>/dev/null
# Linux の systemd ユーザーサービス
systemctl --user stop gh-token-monitor.service 2>/dev/null
systemctl --user disable gh-token-monitor.service 2>/dev/null
rm -f ~/.config/systemd/user/gh-token-monitor.service
次に、GitHubリポジトリに感染コードが混入していないかチェックする。
# 全ブランチで感染ファイルを検索
git log --all --full-history -- "**/.vscode/tasks.json" "**/.claude/setup.mjs"
# 怪しいコミットのパターン(攻撃者が使うコミットメッセージ)
git log --all --oneline --grep="chore: update config"
さらに、IDEのフック経由でパーシスタンスが確立されている場合がある。
# VS Code タスクフック(folderOpenで発火)
cat .vscode/tasks.json 2>/dev/null | grep -A5 "folderOpen"
# Claude Code フック(SessionStartで発火)
cat .claude/settings.json 2>/dev/null | grep -A5 "SessionStart"
これらは npm install --ignore-scripts では防げない。npmとは独立して動作するためだ。感染が疑われる場合は IDE の設定ファイルも必ず確認する。
トークンをrevokeする
デッドマンズスイッチの除去が完了してから、次の順序でrevokeする。
1. npmトークン
# 現在のトークン一覧
npm token list
# 全トークンを失効(npmjs.com の Account Settings からも可)
npm token revoke <token-id>
2. GitHubトークン
GitHub Settings → Developer settings → Personal access tokens で全PAT削除。GitHub Apps・OAuth Appsも確認する。
# GitHub CLI で確認
gh auth status
gh auth logout
3. AWSキー
# 既存キーを無効化
aws iam update-access-key --access-key-id <KEY_ID> --status Inactive
# 新しいキーを作成
aws iam create-access-key
# 古いキーを削除
aws iam delete-access-key --access-key-id <OLD_KEY_ID>
AWS Secrets Manager・SSM Parameter Store・Kubernetesサービスアカウント・HashiCorp Vaultトークンも同様にローテーションする。CI/CD環境のシークレットも忘れずに。
クリーン版に更新する
安全なバージョンにピン留めして再インストールする。
| パッケージ | 侵害バージョン | ピン先(安全版) |
|---|---|---|
| keyv | 6.0.0 | 5.6.0 |
| flat-cache | 6.1.24 | 6.1.23 |
| file-entry-cache | 11.1.6 / 11.1.7 | 11.1.5以前 |
| cache-manager | 7.2.10 | 7.2.9 |
| cacheable-request | 13.0.20 | 13.0.19以前 |
| cacheable | 2.5.1 | 2.5.0以前 |
node_modules を完全に削除して --ignore-scripts オプション付きで再インストールする。
rm -rf node_modules package-lock.json
npm cache clean --force
npm ci --ignore-scripts
--ignore-scripts はlifecycleスクリプト(preinstall/postinstall等)を無効化する。依存パッケージに正規のスクリプトが含まれる場合は動作に影響が出ることがあるが、クリーンインストール時は一度この方法で確認するのが安全だ。
今後のために
今回の攻撃から得られる教訓をまとめる。
preinstallスクリプトを過信しない
npm audit や署名検証が役に立たなかった理由の一つは、攻撃者が乗っ取ったアカウントで正規のSigstore証明書付きプロベナンスを生成したことだ。「誰が署名したか」ではなく「誰がアカウントを乗っ取ったか」が問題だった。ツールチェーンの信頼の連鎖は、最も弱いリンク(メンテナアカウント)で断ち切られる。
TOTPからFIDO2/パスキーへ
攻撃者は標準的なTOTP 2FAをリアルタイムフィッシングで突破した。GitHubのFIDO2/パスキー認証に移行することが最も有効な対策だ。
C2がEthereumスマートコントラクト
C2(コマンド&コントロール)のインフラにEthereumスマートコントラクトが使われていた。固定ドメインが存在しないためシンクホールできない設計で、従来のドメインブロックリストが機能しない。攻撃の高度化が進んでいる。
プロジェクト単位でのignore-scripts設定
# プロジェクトの .npmrc に追記
ignore-scripts=true
CI環境ではデフォルトでこの設定を入れておくと、意図しないpreinstallスクリプトの実行を防ぎやすい。
まとめ
対応の順序をもう一度整理する。
- まずデッドマンズスイッチを除去する(revokeの前)
- IDEのフック(VS Code・Claude Code)を確認する
- npmトークン → GitHub PAT → AWSキーの順でrevoke
-
--ignore-scripts付きでクリーン再インストール
「侵害されたらすぐrevokeせよ」という常識が、このマルウェアには逆効果になる。順番が全てだ。
ESLintを使っているプロジェクトなら今すぐ lockfile を確認してほしい。侵害バージョンが入っていなくても、npm auditが何も言わないことが今回の最大の教訓だ。
あなたのプロジェクトの lockfile に侵害バージョンは含まれていたか?
