動画で読む
結論 — 2026-05-19 朝、私は npm の lockfile を 3 回 grep しました
結論を先に書きます。2026-05-19 の UTC 01 〜 02 に、npm 上で 317 個のパッケージに対して合計 637 個の悪性バージョンが公開されました。最も被害が大きいのは @antv スコープの 271 個と、size-sensor / echarts-for-react / timeago.js / jest-canvas-mock を含む non-scoped 46 個です。私は朝 7 時前にこの一報を見て、AetherEchoes の frontend と隣の業務リポジトリで pnpm-lock.yaml を 3 つ grep して、@antv と echarts-for-react と timeago.js のいずれかが入っていないかを 5 分で確かめました。
やったことを順に書きます。1 つ目は週次で叩いていた pnpm install を一旦止めること、2 つ目は npm publish token と GitHub Actions の OIDC を含む credential 系の棚卸し、3 つ目は pnpm で ignore-scripts=true がリポジトリ root の .npmrc に効いているかの再確認です。前日に npm/PyPI のサプライチェーン攻撃を、個人開発で踏まないために私がやっていること を書いたばかりで、自分が並べた 5 本の防衛策がどこまで効くかを、そのまま試す機会になりました。
何が侵害されたか — @antv 系 271 個と、私が使っている可能性のあった 3 件
要点を先に書きます。今回の攻撃で侵害された 317 パッケージのうち、約 86% は @antv/* のスコープに集中しています。残りの 46 個は scoped でないユーティリティ群で、ここに size-sensor(週 4.2M ダウンロード)、echarts-for-react(3.8M)、timeago.js(1.15M)、jest-canvas-mock が含まれます。@antv/scale(2.2M)と @antv/f2(1.0M)、@antv/g6(1.1M)も対象に入っています。safedep.io の解析記事に全リストがあります。
数字だけ見ると @antv は中国系チャートライブラリの集合で「私は使っていないだろう」と一瞬思ったのですが、実は @antv/g2 を直接使っていなくても、依存の依存に @antv/scale がぶら下がっている可能性があります。私の場合は pnpm why @antv/scale を叩いたら 0 件、pnpm why echarts-for-react も 0 件、pnpm why timeago.js だけ 2 件出ました。timeago.js は、5 年前の私が「軽くて便利」と入れた残り香でした。過去の自分の判断は、未来の自分の手間として降ってくる、という当たり前を再確認しました。
公開された悪性バージョンの大半は 1 パッケージあたり 2 つずつ、合計で 637 個 という数になります。これは前回 npm-pypi-supply-chain-defense-solo-dev で触れた Kevin Patel の風刺記事『No Way To Prevent This』が想定していた「攻撃者は 1 個ずつ刺してくる」という前提よりも、桁が 1 〜 2 つ大きい刺し方です。
仕掛けの中身 — preinstall + Bun + 498KB の難読化スクリプト
要点を先に書きます。今回の悪性パッケージは、package.json の preinstall に bun run index.js を書いて、498KB の難読化された Bun スクリプトを実行する設計です。Bun(Zig 製の JavaScript ランタイム)を選んだのは、最近の dev マシンに高確率で入っていて、Node がインストールされていない環境でも script を回せるからだと思われます。
このスクリプトが何を取りに来るかは、safedep.io の解析に列挙があります。AWS の env / IMDSv2 / ECS metadata / Secrets Manager、GCP の service account、Azure credential、Kubernetes token、HashiCorp Vault、npm token、GitHub token、SSH 鍵、Docker auth、DB 接続文字列、Stripe / Slack の API key、1Password / Bitwarden / pass / gopass のローカル vault まで、と並びます。
取った credential は 2 経路で外に出ます。1 つは攻撃者の GitHub に Dune 命名(sardaukar-sandworm-042 のような)のリポジトリを切って git push する経路、もう 1 つは t.m-kosche[.]com への RSA + AES 暗号化付き HTTPS POST 経路です。さらに systemd または LaunchAgent で kitty-monitor という常駐サービスを置き、GitHub の commit 検索を C2(Command and Control、攻撃者からの遠隔指示口)として動かす設計です。CI/CD では GitHub Actions の OIDC を使って npm publish を試みる、Sigstore で署名を回す、toJSON(secrets) をワークフロー注入で吐き出す、というところまでやります。攻撃者が「次の侵害先」を増やしに行く再帰設計です。
1 つ前の Shai-Hulud キャンペーン(2026-05-01 頃の SAP 侵害)と同じスキャナアーキテクチャと regex 集とフロー制御を使い回しているそうで、safedep.io は「toolkit を battle-tested したものをスケールに振り直した」という言い方をしています。
自分の lockfile で 5 分でやる確認手順
要点を先に書きます。まず lockfile を grep して、該当パッケージ名が出てきたら pnpm why で依存経路を確認、次に preinstall script の有無を node_modules/<pkg>/package.json で確認、最後に payload SHA256 が IoC と一致するかを判定します。私が当日叩いたコマンドをそのまま書きます。
# 1. lockfile に名前があるか
grep -E "@antv/|echarts-for-react|size-sensor|timeago.js|jest-canvas-mock" pnpm-lock.yaml
# 2. 入っていたら依存経路
pnpm why echarts-for-react
# 3. インストール済みなら preinstall を見る
jq '.scripts.preinstall // empty' node_modules/echarts-for-react/package.json
# 4. payload の SHA256 と一致するファイルが node_modules にないか
# safedep.io 公開の IoC: a68dd1e6a6e35ec3771e1f94fe796f55dfe65a2b94560516ff4ac189390dfa1c
find node_modules -name "*.js" -size +400k -size -600k \
-exec shasum -a 256 {} \; \
| grep -i 'a68dd1e6a6e35ec3771e1f94fe796f55dfe65a2b94560516ff4ac189390dfa1c' \
|| echo "OK: no IoC hash match"
4 番目の SHA256 が一致したら、その node_modules は捨てて、pnpm-lock.yaml を直前の git commit に戻して、npm publish token / GitHub Actions の secret を全部 rotate します。私の場合は 4 番目で何も引っかからずに終わりましたが、5 年前の timeago.js をどうするかは別問題として残りました。
当日やった 3 つの対応 — token 棚卸し、CI shut-down、依存差し替え
要点を先に書きます。当日の私は (a) npm publish token と GitHub Actions OIDC の棚卸し、(b) frontend CI の pnpm install 部分を 24 時間止める、(c) timeago.js を date-fns の formatDistanceToNow に置き換えて lockfile を更新、の 3 つを順にやりました。
(a) について。私の npm token は 2 枚あって、1 枚は publish 用、1 枚は read-only。前日に「token は最小権限で」と書いておきながら、publish 用 token の最終利用日を見たら 9 ヶ月前でした。書いた本人が一番守れていない、というよくある光景です。即削除して、必要になったら作り直す方針に切り替えました。npm の 2FA と publish 設定は公式ドキュメントを参照して、auth-and-writes レベルに揃え直しました。
(b) について。今回の侵害は preinstall で発火するので、CI で pnpm install を回した瞬間に CI ランナーごと credential が抜けます。GitHub Actions の job 環境変数(AWS_* / GH_TOKEN 等)が全部対象です。私は frontend の CI を 1 日止めて、npm registry 側で該当バージョンが unpublish されてから再開しました。安全側に倒すコストは「24 時間 deploy が止まる」だけです。1 人開発なら払って惜しくない金額です。
(c) について。timeago.js を date-fns の formatDistanceToNow に置き換えました。差は 5 分です。「5 年前の自分が便利と思って入れたものを、未来の自分がセキュリティで剥がす」という流れは、個人開発を 10 年やっていると 3 〜 4 年に 1 度発生します。今回は 1 件で済みました。
前日の防衛策との接続 — 「半歩前」を続報が埋めにくる
要点を先に書きます。前日に書いた 5 本の防衛策(依存を増やさない / lockfile を読む / postinstall を切る / token を絞る / PR 経由で更新する)のうち、今回の Mini Shai-Hulud に直接効いたのは「postinstall を切る」と「token を絞る」の 2 本でした。
.npmrc に ignore-scripts=true を入れておくと、preinstall も postinstall も走らなくなります。今回の発火点は preinstall: bun run index.js だったので、これだけで火が消えます。ただし ignore-scripts=true は副作用もあって、esbuild / puppeteer / sharp のような binary を build 時に取りに行くパッケージで困ることがあります。私はモノレポ root の .npmrc ではなく、frontend サブパッケージの .npmrc に書く運用にしています。root に書くと CI で sharp が動かなくて 1 時間溶かしたことがあるので、その記憶が指に残っています。設定の詳細は pnpm の npmrc ドキュメントに書かれています。
「token を絞る」の方は前段の (a) で書いた通り、9 ヶ月前の publish token を放置していたので、今回掘り起こされる前に削除しました。残る 3 本(依存を増やさない / lockfile を読む / PR 経由で更新する)は中長期で効く話で、今日の延焼を止める道具ではありません。「半歩前で書いた話の半歩前を、続報が常に埋めてくる」というのが、サプライチェーン攻撃のニュースを 10 年追ってきた肌感です。次の続報が来る前に、書いた本人として書いたことを守れているかを 2 週間に 1 度は確認します。書いた直後は守れていることも、9 ヶ月後はそうとは限らない、という今回の token の件が良い証拠でした。
Tags
よくある質問
- 自分のプロジェクトが影響を受けたか、最短で確認するには?
- `grep -E "@antv/|echarts-for-react|size-sensor|timeago.js|jest-canvas-mock" pnpm-lock.yaml` を叩いて、ヒットしたら `pnpm why <pkg>` で依存経路を確認します。さらに `node_modules/<pkg>/package.json` の `scripts.preinstall` を `jq` で見て、payload SHA256 (`a68dd1e6...`) と一致する 400KB〜600KB の .js が node_modules 内にないかを `find + shasum` で確認するのが最短です。
- 今すぐ `pnpm install` を止める必要はある?
- 該当パッケージが lockfile に無ければ、即座の停止までは要りません。ただし npm 側で侵害バージョンが unpublish されるまで(24 時間程度)、CI での自動 `pnpm install` は止めておくのが安全です。lockfile pin が効いていれば新たに入る心配はありませんが、依存解決経路によっては別ルートから引かれるケースが稀に発生します。
- `.npmrc` に `ignore-scripts=true` を入れれば完全に防げる?
- 今回の `preinstall` 起動は防げます。ただし `sharp` や `esbuild` などの postinstall を期待するパッケージが build できなくなる副作用があるため、フロントエンド向けには有効、ネイティブ binary 系を多用するワーカー側では別の対策(依存 pinning や許可リスト方式)が必要です。私はモノレポの frontend サブパッケージ側だけに書いています。