摘要: 攻擊者在 2026 年 5 月 11 日 19:20 至 19:26(UTC)利用三個弱點,從 TanStack 合法發布管線推送 42 個套件、共 84 個惡意 npm 版本。攻擊鏈結合 pull_request_target、Actions cache 中毒和自 runner 記憶體竊取 OIDC token,載荷則竊取雲端、K8s、Vault、GitHub、npm 憑證與 SSH 私鑰並自我複製。維護者在約 26 分鐘後由外部警報接手,受害版本已 deprecate,安裝過的主機應視為已遭入侵並更換相關憑證。
2026 年 5 月 11 日 19:20 至 19:26(UTC),npm 上接連出現 42 個 TanStack 套件、共 84 個版本的惡意更新。這一路沒有任何 token 被偷:套件是 Router/Start 的 Release workflow 透過 trusted publishing 正常發出去的,registry 端看不到破綻。postmortem 把結論講得很白——三個常見弱點接成一串,就足以接管大型開源專案的發佈管線;當 OIDC 憑證可以從 runner 記憶體裡被整份挖走,「能觸發 workflow 的人」與「能發佈套件的人」就是同一批人。
影響範圍:42 個套件、84 個版本,全數落在 Router 與 Start
中鏢清單整份出自 Router/Start monorepo:包含 @tanstack/history 的 1.161.9 和 1.161.12,合計 42 個套件、每個套件各中兩個版本;query、table 等其他 repo 都確認沒事。帶毒手法是塞進一個偽造的 optional dependency「@tanstack/setup」,使用者一執行 install,2.3 MB 的混淆腳本 router_init.js 便跟著跑起來。
攻擊怎麼成立:三個弱點,環環相扣
第一環是「pwn request」。bundle-size.yml 掛著 pull_request_target,會先 checkout fork 端 PR 的 merge ref 再執行 build,形同讓攻擊者的程式碼在握有 repo 權限的環境裡跑起來,首次貢獻者的審核關卡也一併被繞過。布局比事件本身早一天:攻擊者先透過一個惡意 fork 鋪路,payload 規模約 3 萬行。
第二環是毒化 cache。payload 把檔案寫進 pnpm store,正好對上 release.yml 之後會算出的那把 cache key;PR 與 push 又共用同一份 cache,存取前沒有任何 permissions 檢查,11:29 一筆 1.1 GB 的毒 cache 就此落地。隨後攻擊者把 PR revert 成 no-op、刪掉分支——PR 看起來乾乾淨淨,毒卻留在原地。
第三環是搶走 OIDC token。19:15,Release workflow 重跑,毒 cache 被原樣還原;惡意 binary 掃描 /proc、鎖定 Actions Runner.Worker 行程,把它記憶體裡的 OIDC token 整份倒出(trusted publishing 本來就要靠 id-token: write),再直接 POST 給 npm registry。
載荷的三個本領:收憑證、借 Session 外傳、還會自我複製
router_init.js 的工作是收網:AWS、GCP、K8s、Vault、GitHub token 連同 SSH 私鑰全部收走,透過 Session messenger 以加密方式送出——中途沒有 C2 伺服器可供攔截;接著它還會找出受害者維護的其他套件重新發佈,讓惡意碼自己繁殖開來。
發現與善後:第一批警報來自專案外,距上架約 26 分鐘
第一個警報來自專案外面:StepSecurity 的 ashishkurmi 在 19:46:46 開出 issue #7383,距離首批惡意版本上架大約 26 分鐘。維護者接手止血,21:03 把受害版本全數 deprecate,並發佈 GHSA-g7cv-rxg3-hmpx(CVE-2026-45321,CVSS 9.6);npm 當晚移除 tarball,事件本身在 Hacker News 拿到 1,097 點。凡是在當天裝過任一受害版本的主機,一律該視為已遭入侵,AWS、GCP、Vault、GitHub、npm 與 SSH 憑證全部都要換新。
維護者該補的五個洞
postmortem 裡的自我檢討,比攻擊手法本身更值得一讀:內部對發佈動作完全沒有告警,發現全靠外界;所有 pull_request_target workflow 都該逐一審計、補上 repository_owner 條件守衛;第三方 action 一律釘死在 commit SHA 上;cache 的寫入要用 permissions 管起來;最根本的一項,是重新檢討 trusted publishing 的設計——換成短期 token 搭配人工審查,或加上 provenance 驗證。
