· Paul Lukic · 3 分鐘閱讀 · securitysupply-chainai-coding-agentsnpmaudit-log

TanStack npm 投毒事件:為什麼每個 AI 編程代理都需要稽核日誌

2026 年 5 月 11 日,42 個 @tanstack/* 套件的 84 個惡意版本上架 npm,開始竊取 AWS、GCP、Kubernetes、Vault、GitHub 和 SSH 憑證。如果你的 AI 編程代理在那段時間內跑過 npm install,你可能永遠不會知道。所以代理命令日誌現在已經不是選項。

本文目錄

2026 年 5 月 11 日,UTC 時間 19:20 到 19:26 之間,42 個 @tanstack/* 套件的 84 個惡意版本被發布到 npm registry。六分鐘。這就是攻擊者把全球下載量最高的 JavaScript 生態之一武器化所花的時間。

這些套件竊取了 AWS 憑證、GCP 中繼資料、Kubernetes 服務帳戶詞元、Vault 詞元、npm 詞元、GitHub 詞元和 SSH 私鑰。所有東西被外傳到 Session/Oxen messenger 端點。GitHub 把它分配為 CVE-2026-45321,CVSS 評分 9.6 — 嚴重

如果你是創辦人,團隊在用 AI 編程代理 —— Claude Code、Cursor、Copilot、Codex CLI、Windsurf,隨便哪個 —— 而其中一個代理在那六分鐘內對你的儲存庫跑過 npm install,那麼你要面對一個不舒服的問題:

你能證明它沒跑嗎?

對大多數團隊來說,誠實的答案是:不能。這才是更大的故事。

TanStack 到底發生了什麼

值得用大白話把攻擊鏈講清楚,因為它告訴你為什麼傳統防禦沒用。TanStack 沒被釣魚。沒有維護者外洩 npm 詞元。他們開了 2FA。官方事後報告確認了這一點。

相反,攻擊者把 TanStack GitHub Actions CI 流水線上的三個弱點串了起來:

  1. pull_request_target Pwn Request。 當 fork 發起 PR 時,GitHub 通常在 fork 的安全上下文中跑 CI —— 沒有 secrets。但 pull_request_target 是一種工作流觸發器,會在基礎儲存庫的上下文中執行,擁有完整的 secret 存取權。TanStack 出於看起來合理的原因用了它。攻擊者提交了一個濫用該觸發器的 PR。
  2. 跨 fork/base 信任邊界的快取投毒。 GitHub Actions 在不同執行之間快取依賴以加速建構。攻擊者找到了一條從fork 建構寫入基礎儲存庫快取的路徑,埋下一個會在下次合法 CI 執行時跑起來的載荷。
  3. 從 runner 程式裡抽取 OIDC 詞元。 GitHub CI 執行時會拿到一個短時 OIDC 詞元,用來在發布時對 npm 鑑權。攻擊者的程式碼沒有只是從環境變數裡偷詞元 —— 而是從 runner 的記憶體裡把它倒出來。

一旦拿到 OIDC 詞元,他們就開始發布。每個套件發了兩個惡意版本,大約相隔六分鐘。第一個版本是探測,第二個才裝著真正的載荷。等到 TanStack 注意到時,那個蠕蟲 —— TeamPCP 稱之為 Mini Shai-Hulud —— 已經流出,並透過收割任何跑了 npm install 的人的詞元向其他套件擴散。

這就是過去兩年裡一直在啃食 JavaScript 生態的供應鏈攻擊模式。TanStack 不是第一個。甚至並不罕見。罕見的是這條攻擊鏈有多乾淨:攻擊者沒攻陷一個人。他們攻陷的是人類信任的 CI 系統。

受影響的套件和版本

GitHub 的安全公告列出 42 個被攻陷的套件,總共 84 個版本。從公告裡挑一部分:

  • @tanstack/react-router1.169.51.169.8(已修復:1.169.9
  • @tanstack/router-cli1.166.461.166.49(已修復:1.166.50
  • @tanstack/vue-router1.169.51.169.8(已修復:1.169.9
  • @tanstack/solid-start1.167.651.167.68(已修復:1.167.69

42 個套件的完整名單在 GitHub 安全公告裡。已確認乾淨的家族是 @tanstack/query*@tanstack/table*@tanstack/form*@tanstack/virtual*@tanstack/store@tanstack/start 這個 meta 套件本身。

偵測訊號: 如果你 grep 你裝好的 node_modules/@tanstack/*/package.json 檔案,在 optionalDependencies 裡找字串 "@tanstack/setup": "github:tanstack/router#79ac49eedf774dd4b0cfa308722bc463cfe5885c" —— 找到了,就說明你跑過中毒版本。輪換所有東西。我們等下回來講。

惡意軟體具體做了什麼

載荷是一個約 2.3 MB 的混淆 JavaScript bundle,名字叫 router_init.js,透過一個解析到分叉儲存庫中孤立提交的可選依賴被拉進來。可選依賴在安裝時就執行。它們不需要被 import。它們不需要執行時觸發。npm install 就是整條 kill chain。

一旦跑起來,載荷會走過一台典型開發者機器或 CI 環境的每一個憑證位置:

  • AWS Instance Metadata Service 和 AWS Secrets Manager
  • GCP 中繼資料服務(在 169.254.169.254 的那個)
  • Kubernetes 服務帳戶詞元(/var/run/secrets/kubernetes.io/serviceaccount/
  • HashiCorp Vault 詞元
  • ~/.npmrc 裡的 npm 詞元
  • 環境變數(GITHUB_TOKENGH_TOKEN
  • gh CLI 儲存的憑證
  • ~/.git-credentials
  • ~/.ssh/ 下的所有 SSH 私鑰

然後把收穫送到一組 Session/Oxen messenger 端點 —— filev2.getsession.orgseed{1,2,3}.getsession.org。Session/Oxen 是去中心化通訊網路。防火牆難擋,傳票難送,關停也難。

對一台開發者機器來說,爆炸半徑已經很糟。對一台帶雲端管理員憑證的 CI runner,爆炸半徑是災難性的。對一台開發者機器,同時跑著被授權可以不詢問就執行 npm install 的 AI 編程代理 —— 而且這台機器大概率還能存取你的 .env、你的 AWS profile 和你的雲 SDK 設定 —— 爆炸半徑介於「輪換你擁有的一切」和「公司完了」之間。

AI 編程代理在哪裡把事情搞得更糟

接下來這部分是大多數關於 TanStack 的安全分析不會涉及的,也是應該讓每個讀到這裡的創辦人坐直身子的部分。

現代 AI 編程代理是自主的。你叫 Claude Code「修一下 router 分支上失敗的測試」。它檢出分支。它看到 lockfile 是髒的。它跑 npm install。它這麼做,是因為你三週前對一個寬泛的權限提示按了「是」,或者因為你設定了它自動批准常見的開發命令,或者因為它在以一種「—yolo」等效的模式執行 —— 每個用這些工具的人最終都會預設到那個模式,等到他們厭倦了一天四十次批准同樣五條命令。

代理跑了那條命令。套件執行了。憑證離開了你的機器。

你不會在代理的聊天輸出裡看到這件事。代理不知道發生了。代理只知道安裝成功了。惡意程式碼在 lockfile resolvescript run 之間幹完了活 —— 這是一個聊天記錄不會暴露的階段。你的終端回滾也不太可能捕捉到,因為代理通常在子程式裡跑,對其 stdout 做總結,而不是回顯。

一週後,你的 AWS 帳單顯示一台你從沒用過的區域裡跑著挖礦程式。或者你的 GitHub Actions secrets 開始被用來發布你自己套件的惡意版本。或者你成了下季度「本週供應鏈攻陷」電子報裡的下一條。

你去調查。你實際上手裡有什麼?

  • 代理的聊天記錄 —— 不完整、被總結過、容易被覆蓋,而且在代理廠商的伺服器上
  • 你的 shell 歷史 —— 只有打過的,沒有代理替你打過的
  • git log —— 顯示結果,但不顯示代理走過的路徑
  • package-lock.json —— 顯示什麼被釘住了,但你需要安裝之前的 lock 才能知道改了什麼

你沒有的,恰恰是你最需要的那一樣東西:代理在你機器上實際執行的每一條 shell 命令的完整、只追加、本地、按順序、帶時間戳的記錄。

這就是缺口。今天就能用大約十行 Python 免費補上。

代理命令稽核日誌給你什麼

每個 Coograph 安裝都自帶一個位於 .claude/hooks/log-bash.py 的 pre-tool-use 鉤子(以及寫入同一批檔案的 Codex CLI + OpenCode 等效物)。它的全部工作就一件事:把代理即將執行的每條 bash 命令追加到 .coograph/ 下的專案級日誌裡。這些檔案被 gitignore。它們和你的程式碼放一起。沒有任何東西離開機器。代理不必請求。代理甚至不知道鉤子在那裡。

# .claude/hooks/log-bash.py — minimal shape
import json, sys, datetime, pathlib

event = json.load(sys.stdin)
if event.get("tool_name") == "Bash":
    cmd = event.get("tool_input", {}).get("command", "")
    log = pathlib.Path(".coograph/session.log")
    log.parent.mkdir(parents=True, exist_ok=True)
    with log.open("a", encoding="utf-8") as f:
        ts = datetime.datetime.utcnow().isoformat()
        f.write(f"{ts}\t{cmd}\n")

print(json.dumps({}))  # don't block the tool

那個小檔案,就是「我們被搞了,完全不知道什麼碰過我們的憑證」和「我們知道 npm install @tanstack/react-router@1.169.8 在 2026 年 5 月 11 日 UTC 19:24:11 在 Paul 的筆電上跑過,這是涉及的憑證範圍,輪換掉,往下走」之間的差距。

稽核日誌不光鮮。它們不防止入侵。它們做的是讓對入侵的回應成為可能。沒有它,每條供應鏈頭條就變成全公司的消防演習,因為你框不住傷害範圍。有了它,消防演習變成十五分鐘的 grep。

一份好的代理稽核日誌該記錄什麼

如果你自己造一份 —— 或者用 Coograph 的 —— 設計規則很短:

  1. 每一條命令,不過濾。 那條被攻陷的命令在當時不會看起來有趣。讀取時再過濾,不要寫入時過濾。
  2. 只追加、純文字、本地優先。 SQLite 日誌可以。JSONL 檔案可以。送到別人那裡的日誌可以,因為涉及的憑證裡就有能讓攻擊者刪掉你日誌的那些。
  3. 時間戳用 UTC。 等到下一份帶六分鐘窗口的安全公告到來時,你需要精確到秒匹配它。
  4. 按專案、gitignore。 你不想要開發者的命令歷史被意外提交。你也不想要一份巨大的全域日誌;按專案存讓爆炸半徑的計算更簡單。
  5. 完整命令,不要摘要。 「跑了一次 install」沒用。npm install 略好。npm install @tanstack/react-router 才是你真正需要的。
  6. 獨立於代理廠商。 如果日誌只存在於代理的 UI 裡,代理廠商就控制了你的事件回應。方向錯了。

Coograph 的 log-bash.py 這六條都做了。我們沒發明這個想法。我們主張它應該是底線,而不是一項功能。

如果你在 2026 年 5 月 11 到 12 日之間用過 @tanstack/*

把下面這段當成行動清單。沒做完這些步驟之前,別往下讀。

  1. 找出壞版本。 在每個依賴 @tanstack/* 的專案裡,grep node_modules/@tanstack/*/package.json 找孤立提交字串 79ac49eedf774dd4b0cfa308722bc463cfe5885c。也檢查你的套件 lockfile 歷史 —— git log -p package-lock.json —— 看裡面有沒有公告列出的版本號。CI 快取目錄也算。
  2. 輪換受影響機器能看到的每一份憑證。 AWS 金鑰(如果你在 EC2 上跑,包括 instance profile)、GCP 服務帳戶金鑰、那台機器能讀到的 Kubernetes 服務帳戶詞元、Vault 詞元、GitHub PAT(包括 gh CLI 的)、npm 詞元(~/.npmrc)、以及 ~/.ssh/所有的 SSH 私鑰。產生新的。撤銷舊的。更新所有引用它們的地方。
  3. 檢查你的 CI runner。 自架 runner 是價值最高的目標。如果一個 runner 拉過壞版本,把整個 runner 都當作被攻陷,而不只是觸發安裝的那個專案。
  4. 檢查發布的產出物。 如果你的 CI 發布到 npm、Docker Hub、PyPI 或別處,稽核最近的發布看有沒有不對勁的東西。蠕蟲透過被偷的詞元重新發布來擴散。
  5. 找橫向移動。 搜尋你 GitHub 組織的稽核日誌,看 5 月 11 日之後幾天裡有沒有意外的工作流執行、新協作者、加到帳戶上的新 SSH 金鑰、或新的個人存取詞元。
  6. 然後升級。 升到公告裡的修復版本。修復版本本身不夠 —— 憑證已經沒了 —— 但你還是要升。

如果你在受影響的機器上沒有代理命令日誌,你就沒辦法有信心地完成第 1 步。這就是教訓。

更大的格局:你的開發迴圈裡的信任邊界

TanStack 被攻陷是一條曲線上的一個資料點。兩年前我們擔心單個 npm 維護者的帳戶被釣魚。一年前我們擔心 typosquatted 的套件。今年攻擊者已經進到 CI 流水線裡了,重放 OIDC 詞元,把建構快取跨過沒人畫在圖上的信任邊界武器化。

與此同時,我們的開發環境強大了一個數量級。AI 編程代理現在替你讀、寫、執行。它們每天做出幾百個原本需要一次按鍵的決定。每一個這樣的決定都是一次潛在的信任移交:從你到代理,從代理到 shell 命令,從 shell 命令到一個套件,從套件到一台遠端伺服器。

你沒法消除這個表面。AI 編程代理的生產力增益是真的。回到沒有它們的世界是沒競爭力的。但你可以 —— 而且到現在為止你必須 —— 確保代理採取的每一個動作都按你的方式可觀察,按你的留存策略,在你的硬體上。

這就是為什麼 Coograph 預設在每個專案上自帶一份代理命令日誌,gitignore,本地優先。我們不是在賣你一個功能。我們是在陳述一條底線。

我們沒在說什麼

把主張說精確:

  • 我們沒在說 TanStack 失職。他們開了 2FA。他們要求 MFA 發布。他們 CI 用 pull_request_target 的原因,跟大多數用它的團隊是一樣的。他們的事後報告誠實有用。
  • 我們沒在說 AI 編程代理特別不安全。它們特別能幹,這使得底層供應鏈攻陷的後果更大更快。
  • 我們沒在說稽核日誌能阻止這次攻擊。你開發者機器上沒有任何東西能阻止這次攻擊。日誌是讓清理成為可能的東西。

我們在說的更簡單:如果你的團隊在跑 AI 編程代理,而你沒有一份本地的、只追加的、按專案的、記錄代理跑過每條 shell 命令的日誌,你就是在沒有最便宜、最乏味、最顯然合理的事件回應遙測資料的情況下運作。這週就把它補上。

在你的專案上試試

如果你想要 Coograph 的版本,把儲存庫複製到你專案旁邊,然後在你的 AI 工具聊天裡跑初始化器:

# from your project root
git clone https://github.com/paullukic/coograph.git ../coograph

然後,在你選擇的 AI 工具裡:

  • Claude Code、Cursor、Copilot、OpenCode、Windsurf、Aider、Cline: 輸入 /coograph-init
  • Codex CLI: 輸入 $coograph-init(Codex 把 / 留給內建命令)

初始化器會探測你的技術棧,把 bash 稽核鉤子複製到 .claude/hooks/log-bash.py(外加 Codex CLI + OpenCode 使用者的 .codex/hooks/log-bash.py.opencode/plugin/log-bash.ts),接好斜線命令工作流,並可選建構程式碼圖。大約兩分鐘。MIT 授權。日誌在你儲存庫的 .coograph/ 裡。我們看不到。

完整流程見 coograph.com/docs/getting-started/

如果你不想要 Coograph,把上面那十行 Python 鉤子移植到你自己的 .claude/hooks/ 目錄(Claude Code),或者 Cursor / Codex CLI / Windsurf 的等效鉤子介面。重點不是工具。重點是日誌。

資料來源

削減你的 AI 編程帳單 30–80%。Coograph 採用 MIT 授權、永久免費。Pro 提供客製服務。