- TanStack 的發布 workflow 用了 pull_request_target,讓 fork 來的 PR 也能以主 repo 權限執行,把惡意檔案寫進 CI 共享快取。
- 後續無關的 PR 合併時觸發惡意檔案,從快取偷走 npm publish token,6 分鐘內投毒 84 個 TanStack 套件,最終擴散到 169 個套件、373 個惡意版本,並跳到 PyPI。
- 惡意程式帶 dead man's switch:偵測到你要清理時反而砍掉 home 資料夾,還偽造 Claude Code GitHub app 簽章的 commit 來混淆維護者。
目錄
幾天前,開源維護者最大的惡夢成真了。在短短 6 分鐘內,一批每週合計下載量超過 5,000 萬次的套件被供應鏈攻擊攻陷——而且過程中沒有人被釣魚、沒有密碼外洩、也沒有 token 被直接偷走。更糟的是,這些被投毒的套件是經過簽署、驗證,並透過 npm 的 trusted publishing 機制發布的。而 trusted publishing 這套機制,正是為了防範這類攻擊而設計、被官方推薦了將近兩年的做法。
這次事件被稱為 mini Shai-Hulud。它劫持了 React 生態系中最大的專案之一——TanStack——的發布管線,接著像蠕蟲一樣擴散到數百個套件。本文以 The Code Report(2026 年 5 月 14 日)的說明為事實依據,拆解這個「沒人料到」的攻擊鏈。
TanStack 原本的發布流程
要理解攻擊,得先看 TanStack 正常怎麼發套件:
- 每當一個 pull request 被合併,就會啟動一個 GitHub Actions workflow,負責把新版本發布到 npm registry。
- 為了發布,CI server 得先向 npm 拿一個 publish token。
- 為了證明請求是合法的,GitHub 本身會簽署一份聲明,說明「是哪個 workflow、在哪個 repo、哪個 branch 上執行」。
- npm 拿到這份簽署聲明後,比對組織的 allow list,只有全部吻合才會發出 token。
- 這個 token 只會在 CI 的快取裡存活幾分鐘就失效。
這套設計看起來滴水不漏:token 短命、又不經過人手,傳統釣魚攻擊根本沒有東西可偷。
攻擊怎麼繞過這一切
問題出在觸發條件的設定,而不是 token 本身。攻擊步驟如下:
- 攻擊者 fork 了 TanStack 的 repo,建立一個 pull request,然後立刻把它關掉。
- 儘管這只是個 fork、而且這個 PR 從頭到尾沒有任何人看過——光是「建立 PR」這個動作,就足以啟動發布 workflow。
- TanStack 在設定觸發條件時用了
pull_request_target。這個選項的關鍵在於:任何進來的 PR 都會在「主 repo 的上下文」中、帶著「主 repo 的權限」執行,即使這個 PR 是從 fork 建立的。 - 這些權限,足以讓攻擊者的程式碼把一個被投毒的檔案寫進 CI server 的共享快取——這個快取是 GitHub Actions 用來在不同 job 之間重複使用相依套件的。
- 幾個小時後,一個完全無關的 PR 被合併進 main,觸發了那個被投毒的檔案。它從快取裡撈出 npm publish token,用它一口氣投毒了 84 個全新的 TanStack 套件版本。
換句話說,攻擊者從未真的碰到 token,也沒有攻破簽署機制。他們是讓自己的程式碼「住進」合法流程會經過的快取,再等合法流程自己來執行它。
graph TD
A[攻擊者 fork TanStack 並開一個 PR 後立刻關閉] --> B["pull_request_target 讓 PR<br/>以主 repo 權限執行"]
B --> C[把惡意檔案寫進 CI 共享快取]
C --> D[數小時後一個無關 PR 合併進 main]
D --> E[惡意檔案被觸發<br/>從快取撈出 npm publish token]
E --> F[投毒 84 個 TanStack 套件]
F --> G[使用者 npm install 後<br/>惡意程式掃描系統]
G --> H[找到其他 npm token 就用同樣手法擴散]
從 TanStack 問題變成「所有人的問題」
真正讓它成為蠕蟲的,是感染後的行為。只要你不幸 npm install 了其中一個被投毒的套件:
- 惡意程式就會執行,掃描你的系統,搜刮任何值錢的東西。
- 一旦它找到任何 npm publishing token,就用這些 token、以同樣的手法發布新的中毒版本。
於是攻擊自我複製、跳到下一個維護者身上。第一波受害者包括 Mister AI、UiPath、OpenSearch、Guardrails AI 與 Squawk 的維護者;幾小時內,這些公司也把中毒的套件推上了 npm。而透過它們的 Python SDK,這隻蠕蟲甚至跳到了 PyPI。
到隔天早上,資安公司 Aikido 已追蹤到 169 個套件、共 373 個中毒版本。
蠕蟲還在變聰明
這次攻擊有兩個特別值得注意的「進化」細節:
- 偽造 Claude Code 的簽章:蠕蟲開始偽造由 Claude Code GitHub app 簽署的 commit,讓自己的惡意活動混進維護者早已習慣看到的「AI 生成 commit」裡,更難被一眼識破。
- 鑽進開發工具:在被感染的機器上,它會把自己直接嵌進 Claude Code 與 VS Code。
Dead man’s switch
最陰險的是它埋了一個「dead man’s switch」:在每一台被感染的機器上,只要你一嘗試清理它,它就會把你的 home 資料夾清空。這讓事後的補救變得格外危險——照直覺去移除惡意程式,反而可能觸發破壞。
值得記住的重點
- trusted publishing 不是萬靈丹:token 短命、免人手、經過簽署驗證,這些都擋不住「攻擊者讓程式碼住進 CI 快取、再借合法流程之手執行」的路徑。
pull_request_target是這次的根因:它讓來自 fork 的、甚至沒人看過的 PR,帶著主 repo 的權限與可寫入的共享快取執行。把發布這類敏感流程綁在這個觸發條件上,等於開了一道後門。- 蠕蟲化改變了時間尺度:從 fork 一個 PR 到 84 個套件中毒只花 6 分鐘,並在一夜之間擴散到跨生態系(npm → PyPI)的 169 個套件——遠超過人工監控能反應的速度。
- 清理要格外小心:由於 dead man’s switch 的存在,受影響的機器不該貿然「手動移除」,而應在隔離環境中處理。
參考資料
AI 只根據這篇文章內容回答。點下方任一問題,或直接開右下對話框。
相關標籤
相關文章
AI Agent 的安全防護層怎麼設計:從關鍵字偵測到長期行為監控
拆解 Claude Code、Codex 等 AI agent 的縱深防禦:規則關鍵字、分類器、輸入輸出掃描、執行沙箱、跨對話的長期行為監控,以及 system prompt 與 skills 在其中扮演的角色。
五個不該存在但真的有人做出來的開源專案(The Code Report)
一支 The Code Report 影片盤點了一批「完全沒必要、卻美得莫名其妙」的開源專案:3D 旋轉老鼠游標的終端機、跑在 Tor 上的終端機電話、把上網變成 80 年代科幻恐怖片的擋廣告器、用 Rust 寫 CUDA 的 Nvidia 官方工具,還有把任何歌曲變成 Game Boy 芝麻音的合成器。
被遺忘的開發者如何拯救 JavaScript:Jeremy Ashkenas、Underscore.js 與 CoffeeScript
2009 年的 JavaScript 沒有標準函式庫、沒有模組、沒有類別,每個瀏覽器行為還不一樣。Jeremy Ashkenas 用 Underscore.js 補齊工具函式、再用 CoffeeScript 直接繞過語言本身的爛設計,成為讓 JavaScript 被認真對待的關鍵人物之一。