事後分析: TanStack npm サプライチェーン侵害
(tanstack.com)- 2026-05-11 19:20〜19:26 UTCに、攻撃者が42個の @tanstack/ npmパッケージにまたがって84個の悪意あるバージョンを公開
- 攻撃チェーンは、pull_request_target の「Pwn Request」、GitHub Actionsのキャッシュ汚染、runnerメモリからのOIDCトークン抽出を組み合わせたもの
- npmトークンとpublishワークフローは奪取・侵害されておらず、悪性コードは OIDC trusted publisher 権限でレジストリに直接POSTした
- 影響バージョンをインストールした場合、AWS、GCP、Kubernetes、Vault、GitHub、npm、SSHの認証情報が漏えいした可能性があり、ローテーションが必要
- すべての影響バージョンはdeprecated扱いとなり、npm securityとともにtarballの削除を進めており、追跡IssueとGitHub Security Advisoryが公開された
事案の概要
- 2026-05-11 19:20〜19:26 UTCの間に、攻撃者が42個の
@tanstack/*npmパッケージにまたがって84個の悪意あるバージョンを公開 - 攻撃チェーンは、
pull_request_targetの「Pwn Request」パターン、fork↔baseの信頼境界をまたぐGitHub Actionsキャッシュ汚染、GitHub Actions runnerプロセスのメモリからのOIDCトークン抽出を組み合わせたもの - npmトークンは奪取されておらず、npm publishワークフロー自体も侵害されていないことが確認された
- 悪意あるバージョンは、外部研究者
ashishkurmiがstepsecurityで公開から20分以内に検知した - すべての影響バージョンはdeprecated扱いとなり、npm securityとともにレジストリからのtarball削除を進めている
- 2026-05-11に影響バージョンをインストールしたユーザーは、インストール先ホストからアクセス可能な AWS、GCP、Kubernetes、Vault、GitHub、npm、SSHの認証情報 をローテーションする必要がある
- 追跡Issueは TanStack/router#7383、GitHub Security Advisoryは GHSA-g7cv-rxg3-hmpx
影響範囲
-
影響を受けたパッケージ
- 影響範囲は42個のパッケージと84個のバージョンで、各パッケージにつき2つのバージョンが約6分間隔で公開された
- 全一覧は追跡Issueに含まれている
- 影響を受けていないことが確認された製品群は
@tanstack/query*、@tanstack/table*、@tanstack/form*、@tanstack/virtual*、@tanstack/store、@tanstack/startメタパッケージ @tanstack/start-*は影響なし確認リストには含まれていない
-
悪性コードの動作
- 開発者またはCI環境が影響バージョンに対して
npm install、pnpm install、yarn installを実行すると、npmが悪意あるoptionalDependencies項目を解釈し、fork networkのorphan payload commitを取得する - その後
prepareライフサイクルスクリプトが実行され、影響tarball内に隠された約2.3MBの難読化されたrouter_init.jsが動作する - 悪性スクリプトは、AWS IMDS/Secrets Manager、GCP metadata、Kubernetes service-account token、Vault token、
~/.npmrc、GitHub token、ghCLI、.git-credentials、SSH private key など一般的な場所から認証情報を収集する - 奪取データは Session/Oxen messenger file-upload network を通じて流出し、送信先は
filev2.getsession.org、seed{1,2,3}.getsession.org - このネットワークはエンドツーエンドで暗号化され、攻撃者制御のC2も存在しないため、ネットワーク上の緩和策はIP/ドメイン遮断に限られる
- 自己伝播ロジックは
registry.npmjs.org/-/v1/search?text=maintainer:<user>で被害者が管理する他のパッケージを列挙した後、同じ注入方式で再公開する - payloadはnpm installライフサイクルの一部として実行されるため、2026-05-11に影響バージョンをインストールしたホストは潜在的に侵害されたものとして扱う必要がある
- 開発者またはCI環境が影響バージョンに対して
タイムライン
-
攻撃前: キャッシュ汚染段階
- 2026-05-10 17:16 UTCに攻撃者がTanStack/routerのforkである github.com/zblgg/configuration を作成し、fork一覧の検索を避けるために名前を変更
- 2026-05-10 23:29 UTCに細工された身元
claude <claude@users.noreply.github.com>で悪性コミット65bf499d16a5e8d25ba95d69ec9790a6dd4a1f14がforkに作成される - このコミットは約30,000行のバンドルJSペイロード
packages/history/vite_setup.mjsを追加し、pushイベントのCIを抑止するためコミットメッセージに[skip ci]を付けた - 2026-05-11 10:49 UTCごろに
zblggがTanStack/routermainを対象に「WIP: simplify history build」というタイトルのPR #7378を作成 bundle-size.ymlとlabeler.ymlはどちらもpull_request_targetでPRに対して自動実行され、pull_request_targetは初回コントリビューター承認ゲートを回避するため別途承認は不要だったpull_requestを使うpr.ymlは承認待ち状態でブロックされ、実行されなかった- 2026-05-11 11:01〜11:11 UTCに
zblggがPR headへ複数回force-pushし、追加のpull_request_target実行を誘発 - 2026-05-11 11:11 UTCに悪性コミット
65bf499dがPR headに載り、bundle-size.ymlのbenchmark-prジョブがrefs/pull/7378/mergeをチェックアウトした後、pnpm installとpnpm nx run @benchmarks/bundle-size:buildを実行し、その過程でvite_setup.mjsが動作 - 2026-05-11 11:29 UTCに
Linux-pnpm-store-6f9233a50def742c09fde54f56553d6b449a535adf87d4083690539f49ae4da11という1.1GBのGitHub ActionsキャッシュがTanStack/routerに保存される - このキャッシュは
refs/heads/mainスコープに保存され、次のmainpushでrelease.ymlが参照するキーに一致するよう構成されていた - 2026-05-11 11:31 UTCに攻撃者はPRを当時の
mainHEADであるb1c061afに戻し、可視上のPRを0ファイル変更のno-opのように見せかけ、同じ分にPRを閉じてbranchを削除したが、汚染されたキャッシュは残った
-
発火: 公開段階
- 2026-05-11 19:15 UTCにManuelがPR #7369をマージして
mainpushが発生し、release.ymlワークフロー実行25613093674が19:15:44に開始された後、失敗した - 2026-05-11 19:20:39 UTCにnpm registryが
@tanstack/history@1.161.9と41個の兄弟パッケージのpublishを受信 - 全体では42個のパッケージにまたがって約84バージョンが公開されたが、この正確な秒に見えているのは約半分で、残りは2回目のrunで公開された
- publish認証はTanStack/router
release.yml@refs/heads/mainに対する OIDC trusted-publisher binding によって行われたが、テスト失敗のためスキップされたワークフローのPublish Packagesステップで発生したものではなかった - 実際の公開者はテスト/クリーンアップ段階で実行されたマルウェアであり、
id-token: write権限でOIDCトークンをmintした後、registry.npmjs.orgに直接POSTした - 2026-05-11 19:20:47 UTCにrun
25613093674はfailure状態で完了 - 2026-05-11 19:16 UTCにManuelがPR #7382をマージして2回目の
mainpushが発生し、19:16:22にワークフロー実行25691781302が開始 - 2回目のrunも同じ汚染キャッシュをrestoreし、2026-05-11 19:26:14 UTCに
@tanstack/history@1.161.12など、パッケージごとの2つ目のバージョンセットが同じOIDCメカニズムで公開された - 2026-05-11 19:26:20 UTCにrun
25691781302もfailure状態で完了
- 2026-05-11 19:15 UTCにManuelがPR #7369をマージして
-
検知と対応
- 2026-05-11 19:50 UTCごろに外部研究者
carliniが、悪性のoptionalDependenciesフィンガープリントとパッケージ一覧を含むIssue #7383を作成 - 初期リストは42個中14個で、研究者はnpm securityにも直接通知
- 2026-05-11 20:00 UTCごろにManuelが #7383 でインシデントを確認し、対応を開始
- 2026-05-11 20:10 UTCごろにManuelはユーザーマシン侵害の可能性に備え、他チームメンバーのGitHub push権限を削除
- 2026-05-11 20:30 UTCごろにTannerが完全なIOC一覧とregistry側tarball削除リクエストを security@npmjs.com に送り、npm経由で正式なマルウェア報告を提出
- 2026-05-11 21:00 UTCごろに295個の
@tanstack/*パッケージ全体のスキャンにより、範囲が42個のパッケージ、84バージョンであることが確認された - Tannerは84個の影響パッケージすべてに対するnpm deprecationを開始し、
@tan_stackとメンテナーたちがTwitter/X、LinkedIn、Blueskyで公開通知を行った - 2026-05-11 21:30 UTCに
bundle-size.ymlのpull_request_targetキャッシュ汚染ベクターとzblgg/configurationforkが特定された - すべてのTanStack/* GitHub repositoryのキャッシュ項目がAPIで削除された
- hardening PRがマージされて
bundle-size.ymlが再構成され、repository_ownerガードが追加され、third-party action refがSHAに固定された - 公式のGitHub Security Advisoryが公開され、CVEが要求された
- 2026-05-11 19:50 UTCごろに外部研究者
根本原因
-
3つの脆弱性の結合
- 攻撃には3つの脆弱性すべてが必要で、どれか1つだけでは不十分だった
- fork PRのコードがbase repository cacheへ渡り、base repository cacheがrelease workflow runtimeへ渡り、release workflow runtimeがnpm registryへの書き込み権限につながるという形で、各脆弱性が互いの信頼境界を接続していた
-
pull_request_targetの「Pwn Request」パターンbundle-size.ymlはfork PRに対してpull_request_targetで実行され、そのtrigger context内でforkのPR merge refをcheckoutした後にbuildを実行していた- 主要な構造は次のとおり
on: pull_request_target: paths: ['packages/**', 'benchmarks/**'] jobs: benchmark-pr: steps: - uses: actions/checkout@v6.0.2 with: ref: refs/pull/${{ github.event.pull_request.number }}/merge # fork's merged code - uses: TanStack/config/.github/setup@main # transitively calls actions/cache@v5 - run: pnpm nx run @benchmarks/bundle-size:build # executes fork-controlled code- workflow作成者は
comment-prjobとbenchmark-prjobを分離して信頼境界を分けようとしており、YAML commentにはbenchmark-prを「untrusted with read-only permissions」として維持しようとする意図が記されていた - しかし、
actions/cache@v5のpost-job saveはpermissions:では防げず、cache writeにはworkflowのGITHUB_TOKENではなくrunner内部tokenが使われる - したがって、
permissions: contents: readの設定ではcacheの改変を防げない - cache scopeはrepository単位で、base repository cache scopeを使う
pull_request_targetrunとmainpushが共有する - base repository cache scopeで実行されるPRは、後に
mainのproduction workflowがrestoreするcache entryを汚染できる
-
GitHub Actionsキャッシュ汚染
- 悪意ある
vite_setup.mjsは、正常なrelease.ymlworkflowが計算・参照するpnpm-store keyに合わせてデータを書き込むよう設計されていた - 対象keyは
Linux-pnpm-store-${hashFiles('**/pnpm-lock.yaml')}形式 benchmark-prjob終了時に、actions/cache@v5post-stepが汚染されたpnpm storeをまさにそのkeyで保存する- その後
mainpushでrelease.ymlが実行されると、Setup Toolsstepが設計どおりに汚染されたentryをrestoreした - この攻撃タイプは2024年にAdnan Khanが文書化したGitHub Actions cache poisoning系であり、TanStack固有のバグではなく、意識的な緩和が必要なGitHub Actionsの設計上の問題である
- 悪意ある
-
runnerメモリからのOIDCトークン抽出
release.ymlはnpm OIDC trusted publishingに必要なため、正当にid-token: writeを宣言していた- 汚染されたpnpm storeがrunnerにrestoreされると、攻撃者制御のbinaryがディスク上に存在することになり、build stepで呼び出される
- そのbinaryは
/proc/*/cmdlineでGitHub ActionsのRunner.Workerプロセスを見つけ、/proc/<pid>/mapsと/proc/<pid>/memを読み取ってworkerメモリをdumpする - その後、runnerが
id-token: write設定のもとでlazy mintしたOIDCトークンをメモリから抽出する - 抽出したtokenで
registry.npmjs.orgへの直接POSTリクエストを認証し、workflowのPublish Packagesstepを完全に迂回する - このメモリ抽出方式は2025年3月の
tj-actions/changed-filescompromiseで使われた方式と同じで、attribution commentを含む同一のPython scriptが使われていた - 攻撃者は新しい手法を発明したのではなく、公開研究を再構成していた
-
各要素が単独では不十分な理由
pull_request_target自体はlabelやcommentのような信頼された作業に使うことはできる- すでに侵害されたdependency内部でのcache poisoningだけでは、別のpublish vehicleが必要になる
- OIDC token extractionだけでは、runner上での既存のcode executionが必要になる
検知とIOC
-
検知経路
- 検知は内部ではなく外部から行われた
carliniがpublish後およそ20分でissue #7383を立て、完全な技術分析を提供した- Tannerはwar roomを開始した直後に、Socket.devから状況確認の電話を受けた
-
downstream maintainerとセキュリティツール向けfingerprint
@tanstack/*パッケージmanifestでは、次のoptionalDependencies項目が主要なIOCである
"optionalDependencies": { "@tanstack/setup": "github:tanstack/router#79ac49eedf774dd4b0cfa308722bc463cfe5885c" }- ファイルIOCはpackage rootの
router_init.jsで、約2.3MBあり、"files"には含まれていない - cache keyは
Linux-pnpm-store-6f9233a50def742c09fde54f56553d6b449a535adf87d4083690539f49ae4da11 - 第2段階payload URLは
https://litter.catbox.moe/h8nc9u.js,https://litter.catbox.moe/7rrc6l.mjs - 流出先ネットワークは
filev2.getsession.org,seed{1,2,3}.getsession.org - 偽装されたcommit identityは
claude <claude@users.noreply.github.com>で、実際のAnthropic Claudeではなく、細工されたGitHub no-reply emailである - 実際の攻撃者アカウントは
zblggid 127806521,voicproducoesid 269549300 - 攻撃者のforkは github.com/zblgg/configuration で、TanStack/router forkを検索回避目的でrenameしたもの
- fork network内のorphan payload commitは
79ac49eedf774dd4b0cfa308722bc463cfe5885c - 悪意あるpublishを実行したworkflow runは github.com/TanStack/router/actions/runs/25613093674 のattempt 4 と github.com/TanStack/router/actions/runs/25691781302
教訓
-
うまくいった点
- 外部の研究者がインシデント発生後約20分以内に検知し、技術的な詳細全体とともに報告した
- maintainer team が複数の time zone にまたがって即座に連携した
- 検知コミュニティが数時間以内に明確な公開 IOC パターンを確保した
-
改善が必要だった点
- 内部 alerting がなく、compromise の事実を第三者から知らされた
- 独自の publish monitoring が必要であり、この種の問題を迅速に検知できるエコシステムのセキュリティ研究企業とより緊密に協力し、feedback loop を短くする計画である
pull_request_targetworkflow は以前から危険なパターンとして知られていたが、audit されていなかった- third-party action の floating ref である
@v6.0.2、@mainは今回の事件とは別に、常時 supply-chain risk を生み出している - npm の「dependent がある場合は unpublish 不可」という方針のため、ほぼすべての影響パッケージで unpublish が不可能だった
- registry 側での tarball 削除を npm security に依存する必要があり、その結果、悪性 tarball がインストール可能な状態のまま残る時間がさらに数時間増えた
- npm scope の7人の maintainer 一覧は、同じ blast radius に対して7つの個別 credential-theft target を作ることを意味した
- OIDC trusted-publisher binding には publish ごとの review がなく、一度設定されると workflow 内のどの code path でも publish 可能な token を mint できる
- 必要な代替策は、手動 review を伴う短期 classic token へ移行するか、想定外の workflow step での publish を検知する provenance-source-verification を追加することである
-
幸運だった点
- 攻撃者がテストを壊す payload を選んだため、通常の publish step が skip され、よりクリーンに見える tarball は生成されなかった
- その結果、攻撃が十分に目立つ形で表面化し、迅速に検知された
- より慎重な攻撃者がテストを壊さなければ、さらに数時間ひそかに publish を続けられた可能性がある
- 攻撃者は attribution comment を含む公開 memory-dump script を再利用し、新しいコードを書かなかったため、IOC matching がより速く進んだ
残る疑問
bundle-size.ymlのSetup Toolsstep が実際にactions/cache@v5を呼び出したのか確認する必要がある- PR #7378 に対する
pull_request_targetrun の1つの post-job log を読んで検証する必要があり、例の run id は25666610798である - force-push で消える前の最初の PR head commit に何が含まれていたのか確認する必要があり、GitHub reflog に残っている可能性がある
- 悪性 commit が fork の git object store に入った方法が、直接の git push だったのか、audit-log entry を残す GitHub web UI での作成だったのかを確認する必要がある
voicproducoesが実在するアカウントなのか sock puppet なのか、活動履歴と照合する必要がある- 6つの重複した
linux-npm-store-*entry に見える npm cache も汚染されていたのか、実際に使われたのかを確認する必要がある - 攻撃に Nx Cloud が必要だったのか、GitHub Actions cache だけでも機能したのかを確認する必要がある
- TanStack/router の fork network 内で、orphan payload commit を含む別の fork を特定できるのか確認する必要がある
- 他の fork がその commit を hosting している場合、
github:tanstack/router#79ac49ee...へのアクセス可能性が維持され、cleanup はさらに難しくなる - router、query、table、form、virtual など他の TanStack repo でも同じ
bundle-size.ymlスタイルのパターンを使っているか audit が必要である - publish window の間に影響バージョンを実際にダウンロードしたユーザー数を npm support から取得する必要がある
- 7人の maintainer のマシンが個別に侵害されていないか確認する必要がある
- 悪性 publish では maintainer の npm token は使われなかったが、maintainer のマシンは self-propagation logic の二次 target である可能性がある
参考資料
- TanStack/router#7383: 追跡 issue
- GHSA-g7cv-rxg3-hmpx: GitHub Security Advisory
- The Monsters in Your Build Cache: Github Actions Cache Poisoning: Adnan Khan による 2024 年の GitHub Actions cache poisoning 研究
- Keeping your GitHub Actions and workflows secure: Preventing pwn requests: GitHub Security Lab の pwn request 防止資料
- Harden-Runner detection: tj-actions/changed-files action is compromised: StepSecurity による 2025 年 3 月の関連検知資料
- Unpublish: npm unpublish ポリシー
- Provenance: npm provenance ドキュメント
- GHSA-g7cv-rxg3-hmpx: 影響バージョン全体の一覧を扱う GitHub Security Advisory
1件のコメント
Hacker Newsの反応
トークンを失効させるときは注意が必要。ペイロードは dead-man's switch を
~/.local/bin/gh-token-monitor.shにインストールし、Linux では systemd ユーザーサービスとして、macOS ではLaunchAgent com.user.gh-token-monitorとして登録するように見える盗んだトークンで 60 秒ごとに
api.github.com/userをポーリングし、トークンが失効して HTTP 40x が返るとrm -rf ~/を実行するhttps://github.com/TanStack/router/issues/7383#issuecomment-...
今後 5 年のソフトウェアの世界は本当に荒れそうで、エアギャップシステム の重要性が大きく増しそうだ
@mistralai/mistralainpm パッケージもこの ワーム の一部として侵害されたhttps://github.com/mistralai/client-ts/issues/217
現在は npm レジストリから削除されている
残念だが、これは Trusted Publishing だけでは CI で安全にデプロイするのに十分ではない証拠に見える。CI パイプライン内の攻撃者や、盗まれたリポジトリ管理者権限があれば容易にデプロイできる
新しい話ではなく、Trusted Publishing もそれを保証するよう設計されているわけではないが、ローカルデプロイと 2 要素認証から Trusted Publishing に移ると、CI 侵害を通じたこの種の攻撃経路が生まれる。ローカル作業時に npm publish を防いでくれていた第 2 要素が消えることになる
現在の展開を見ると、攻撃者は CI/CD パイプラインを掌握しており、npm publish に第 2 要素がなかったため、OIDC トークンを盗んでデプロイを完了したように見える。興味深いが別の話として、デプロイジョブ自体は失敗した一方で、悪意あるコミット内のペイロードがワークフローの OIDC トークンで自力デプロイできたようだ
求められているのは、長期トークン不要の Trusted Publisher モデルを維持しつつ、GitHub の外側に第 2 要素が残る CI デプロイだ。つまり、誰かが npm 側で 2 要素認証を使ってアーティファクトを実際の公開状態へ昇格させる 段階的デプロイ が必要になる
デプロイが GitHub の信頼モデルの中だけで可能なら、リポジトリ管理者トークンを奪った人やパイプラインに悪意あるコードを入れた人は誰でも簡単にデプロイを完了できる。GitHub コンテキスト外の本物の第 2 要素があれば、リポジトリを壊したりマルウェアを仕込んだりはできても、レジストリ用の第 2 要素なしではデプロイできない
事後分析: https://tanstack.com/blog/npm-supply-chain-compromise-postmo...
TanStack パッケージを取得または同梱していた可能性のある下位パッケージについて、安全と見なしてよい証拠があるのか気になる
postinstall スクリプト は致命的だ。全員 pnpm を使うべきだ
FORK に push された「孤児」コミットが npm クライアントでこんなことを引き起こせるのは信じがたい。GitHub にも大きな責任があると思う。悪意あるフォークのコミットが、GitHub の共有オブジェクトストア経由で正規リポジトリと区別のつかない URI でアクセスできるのは完全に狂った設計だ
最初に読んだときは「fork」という言葉を誤用していて、実際には公式リポジトリのブランチを指しているのだと思った。それが本当なはずがないと思ったのに、まさか
https://tanstack.com/blog/npm-supply-chain-compromise-postmo...
TanStack がこの件に関する 事後分析 をたった今公開した
npm 環境を安全に設定しろという注意喚起
https://gajus.com/blog/3-pnpm-settings-to-protect-yourself-f...
いくつか設定するだけで大きな問題を減らせる
allow-git=noneもある: https://github.blog/changelog/2026-02-18-npm-bulk-trusted-pu...min-release-age。2) なぜか分単位ではなく日単位になっている: https://docs.npmjs.com/cli/v11/using-npm/config#min-release-...依存関係マネージャーの世界は、まったく不必要に断片化されていると思う
パッケージのバージョン依存が
^1.0.0のようになっていたり、まして"*"だったりするなら、続きを読む前にすぐ安全なバージョンへ固定すべきだClaude で急いで作って拡散抑制の助けになればと思った。もちろん自分で検証する必要はあるが、言及された 侵害パッケージ がマシン上にあるかスキャンしてくれる: https://github.com/PaulSinghDev/tanstack-shai-hulud-fix
もう各プロジェクトを 個別の VM で動かす段階に来たように思える
最近のローカル権限昇格脆弱性を見ると、Docker だけではまったく不十分だ。そもそもコンテナは主要なセキュリティ境界として設計されていない
コンテナ内からアクセスが必要な他のクラウドサービスがあるなら、この資格情報窃取機はそれも持っていくだろう。それでも 被害半径 は減らせるので、少なくとも改善にはなる
すごい、また別の巨大パッケージだ。Axios と LiteLLM が侵害された後に投稿した公益的な注意喚起を再掲しておく。ライフサイクルスクリプト に関する話も当てはまる
npm/bun/pnpm/uv はいまやすべて、パッケージの最小リリース年齢設定をサポートしている。
~/.npmrcにignore-scripts=trueも入れてあり、分析ではこれだけでも脆弱性を緩和できたはずだ。bun と pnpm はデフォルトでライフサイクルスクリプトを実行しないグローバル設定で最小リリース年齢を 7 日にする方法は次のとおり
~/.config/uv/uv.tomlexclude-newer = "7 days"~/.npmrcmin-release-age=7 # daysignore-scripts=true~/Library/Preferences/pnpm/rcminimum-release-age=10080 # minutes~/.bunfig.toml[install]minimumReleaseAge = 604800 # secondsグローバル設定を上書きする必要があるなら CLI フラグを使えばよい
npm install --min-release-age 0pnpm add --minimum-release-age 0uv add --exclude-newer "0 days"bun add --minimum-release-age 0もう 1 つ付け加えると、依存関係の待機時間を大規模に導入すると脆弱性発見が遅れたり、依存関係の待機時間が一種のフリーライドではないかという懸念があるようだが、私は同意しない。依存関係の待機時間で交換しているのは 時間選好 であり、私より常に時間選好の高い人たちは存在する
0: https://news.ycombinator.com/item?id=47582220
1: https://news.ycombinator.com/item?id=47513932
予期しない変更を避けるには
pnpm install --frozen-lockfileを使えばよい。min-release-ageを設定していないと、間接依存を通じても影響を受けたパッケージを引き込む可能性があることを覚えておくべきだ。可能ならパッケージマネージャーのバージョンも固定したほうがよい