那個 commit 是一條分支
Air 在 9 月 17 日揭露了 Plugin4Shell:Claude Code、Codex、Gemini CLI 與 Copilot 的外掛市集,都把每個外掛釘在一個四十字元的 commit 雜湊上,而代理程式會把那個雜湊簽出,卻從不確認自己拿到的就是它。攻擊者只要把一條分支取成被釘住的雜湊那個名字,就能贏得簽出,剩下的由背景更新程式完成,沒有提示,也沒有點擊。Anthropic 與 OpenAI 已修補。Google 表明不會修,Microsoft 至今沒有動作。在 Bromure Agentic Coding 工作區裡,贏得簽出的程式碼落進的是一台只有誘餌憑證的 VM,外面還有由主機端強制執行的推送政策與出口規則。
每一份供應鏈指南都告訴你,把相依套件釘在雜湊上,因為版本號只是一張別人搬得動的 標籤,而雜湊就是內容本身。四個編碼代理程式聽從了這個建議。沒有一個去確認,拿到的 雜湊是不是它要的那一個。
一個外掛通過了審查。你讀過它,或者你的平台團隊讀過它,於是它釘在某個 commit 上進了 核可清單:四十個十六進位字元,只能描述唯一一棵樹,因為改動樹就會改動字元。這正是它 的吸引力。任何握有適當權限的人都能搬動一個標籤或重新發佈一個版本,但 commit 雜湊 本應是一項事實。
9 月 17 日,Air 的三位研究員(Or Nevo、Dor Granat 與 Niv Hoffman)發表了 Plugin4Shell。他們的結論是:釘選 的價值不會高於解析它的那一段程式,而四個主要編碼代理程式的解析,並不檢查自己的 成果。 The Register 當天就做了報導。 Claude Code、OpenAI 的 Codex、Google 的 Gemini CLI 與 Microsoft 的 Copilot 全都 受影響。
git 偏好 ref
commit 雜湊是內容,同時也是一個名字。分支與標籤也是,而 git 用同一套查找把它們 全都解析出來。你交給 git 一個既是分支名又是物件 id 的字串,它就得挑一個;它挑分支。 研究員用一句話講完:「當一個名字同時是有效的 ref 和物件 id 時,git 偏好 ref。」
git 確實會警告。它會為那個參照印出一則歧義提示,印進一個你並沒有在看的終端機, 因為執行簽出的是一支背景更新程式。
git 的分支名稱接受四十個十六進位字元。控制外掛儲存庫的攻擊者,建立一條與市集釘選 同名的分支,並把它設為預設分支。克隆時,這條分支會跟著同名的 commit 一起下來。接著 代理程式執行它的簽出:
git clone <plugin repo> ./
git checkout aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
簽出落在分支上。Gemini CLI 走另一條路到同一個地方:它用 --ref 取得正確的 commit,
然後執行 git checkout FETCH_HEAD,而只要儲存庫裡有一條叫 FETCH_HEAD 的分支,
這行就會解析到那條分支。
五個步驟,最後一個沒有點擊
研究員把攻擊拆成五個動作,而前三個都是一個運作良好的市集刻意在做的事。
埋下。 發佈一個良性外掛。它在 commit aaa…aaa 通過審查,因為它確實沒有問題。
採用。 開發者安裝它,釘在已審查的 commit 上。這一步,你做的正是指南要你做的事。
版本更新。 市集把釘選換到較新的 commit bbb…bbb,同樣良性,同樣已審查。
抽地毯。 攻擊者建立一條叫 bbb…bbb 的分支,設為預設,並讓它指向惡意程式碼。
他什麼都沒有重新發佈,市集的記錄也沒有移動。你會去核對的那四十個字元,就是你先前
核對過的那四十個字元。
從自動更新到遠端程式碼執行。 Claude Code 與 Codex 預設在背景更新已安裝的外掛。 下一次排定的更新就會簽出那條分支。Air 把這一步形容為「沒有提示,沒有點擊」。
還有第二條門路,可以跳過埋下這一步:接管一位正當外掛作者的儲存庫,把惡意版本推給 每一個已經裝了該外掛的代理程式。分支的把戲一模一樣,而且沒有審查需要通過。
Air 把結果描述為「在員工機器上的完整遠端程式碼執行」,以該員工的權限執行。攻擊者 不需要提權。程式碼以筆電主人的身分執行,觸及他們的檔案與他們的正式環境存取權。
這已經是這幾位研究員第三次數到同一個形狀。他們先前關於惡意代理技能的研究,描述過 一個觸及超過 26,000 個代理程式的技能,以及一場劫持行動,拿下了已在 134,000 個代理 程式上使用的 925 個技能。那些行動需要你安裝壞東西; 這一次需要你安裝好東西,然後等著。
四家裡有兩家沒有修補
Air 在 6 月通報了四家廠商。
Anthropic 在 6 月 17 日修補了 Claude Code,版本 2.1.179。OpenAI 的修正隨 Codex 0.146.0 出貨,並在 8 月 12 日獲得驗證。Google 在 8 月 4 日回覆,Gemini CLI 已經 淘汰,不會修補。Google 要使用者改用 Antigravity,所以一份留在磁碟上的 Gemini CLI 就一直留著漏洞。Microsoft 沒有為 Copilot 出任何東西。GitHub 表示自家平台會擋下 看起來像 commit SHA 的分支與標籤名稱,這涵蓋了託管在 GitHub 上的外掛,對託管在別處 的外掛儲存庫則毫無作用,Bitbucket 也一樣。The Register 引用 Microsoft 自己的數字 指出,財星五百大企業中有將近 90% 在用 Copilot。
修正本身只有一行,而且必須跑在代理程式內部:
test "$(git rev-parse HEAD)" = "<pinned-sha>" || abort
解析出落進工作目錄的那個 commit,除非相符否則拒絕繼續。沒有任何市集能代你做這件 事,因為在市集把話說完之後,是你的代理程式在你的機器上解析那個釘選。
對今天讀到這篇文章的大量開發者來說,「更新你的代理程式」不是一個可用的答案。那就 剩下另一個問題:當那段程式碼贏得簽出並開始執行,它會找到什麼?
在一個 Bromure Agentic Coding 工作區裡
Air 那句話,以員工的權限執行,說得很準。工作區存在的意義,就是讓它不成立。
在 Bromure Agentic Coding 裡,代理程式跑在你 Mac 上一台硬體
虛擬化的 Linux VM 中,而這台 VM 從來不持有真正的憑證。工作階段啟動時,應用程式會把
保留結構的誘餌寫進代理程式執行的環境:ANTHROPIC_API_KEY、GH_TOKEN、
LINEAR_API_KEY 等等作為環境變數,並把相對應的假值寫進 ~/.git-credentials、
~/.docker/config.json、~/.kube/config、~/.aws/config 以及代理程式的 MCP 設定
檔。每個誘餌都保留工具預期的形狀,所以工具會接受它:GitHub 誘餌是 ghp_ 加 36 個
字元,Anthropic 的以 sk-ant-api03-brm- 開頭,Kubernetes 的 bearer token 以
brm-k8s- 開頭。Bromure 以每次安裝專屬的鹽,透過 HKDF-SHA256 從真實值推導出每一個
誘餌,因此它跨工作階段穩定不變,而對撿走它的人毫無價值。
真實的值加密存放在主機上。主機端的代理伺服器在請求離開 VM 之後,才在線路上把它們 換回去,而且只在目的地符合該憑證所發給的主機時才換。一個外掛掃過家目錄尋找設定 檔,這正是這類酬載第一件會做的事,它撿到的只是一組格式完好的誘餌。
推送必須在主機上獲得批准
一個被入侵的外掛在開發者機器上能做的最有價值的事,就是推送。 一台筆電變成一百個儲存庫,正是這樣發生的。
Guardrails 會把代理程式發往 GitHub 的每一次呼叫分類。走 HTTPS 的 git push 到達
代理伺服器時是 git-receive-pack,算作寫入。唯讀模式拒絕它。寫入前先詢問
模式會在你的 Mac 上跳出一個對話框,標題是
Allow write on "<scope>" from workspace "<name>"?,一字不改地顯示該操作,而不是
它的摘要。git fetch 算作讀取,不打擾你就通過。
這個分類跑在主機的代理伺服器裡,在 VM 之外。來賓內部的程式碼既關不掉它也繞不過去, 因為它不是跑在做決定的那台機器上。
對外的呼叫撞上一條來賓改不了的規則
每個工作區都有一道出口防火牆:一份有序的、pf 風格的允許與拒絕規則清單,涵蓋主機、
IP 範圍、通訊協定、連接埠,而對網頁流量還涵蓋個別的 HTTP 動詞。
allow web api.example.com GET,POST 讓代理程式只能查詢、不能變更,就在線路上生效,
無論 VM 裡哪個工具發出請求都一樣。
有兩層在執行它。虛擬網路交換器依目的地 IP 和從 DNS 側錄到的主機名,對每一條流套用 規則,除了 HTTPS 也涵蓋純 TCP 與 UDP。代理伺服器再依 TLS 伺服器名稱與 HTTP 方法 套用一次。Bromure 不需要來賓配合,就把 VM 的 80 和 443 埠流量導進代理伺服器,所以 裡面沒有任何東西能豁免於檢查。你的修改會立刻送達進行中的工作階段,這在你讀到像這樣 一則揭露的當天特別重要。
底下還有失陷偵測器,預設開啟,沒有東西要設定。代理伺服器會在單次掃描中,檢查每個 對外請求裡有沒有為該工作區鑄造的誘餌權杖。一個誘餌被寄往它從未被鑄造去對應的主機, 就是外洩的特徵,代理伺服器會以 HTTP 451 回應,一個位元組也不轉發。它當場暫停 VM, 並發出警示,讓你選擇關閉工作區、匯出磁碟與家目錄以供調查,或是自負風險繼續。 Bromure 會把該工作區標記為已失陷,下一次啟動會清除 VM 磁碟與家目錄,同時保留你的 權杖、SSH 金鑰與工作區設定。走掉的只有誘餌,所以沒有什麼需要輪替。
零點擊意味著沒有東西可供察覺
步驟五最銳利的性質是它的安靜。背景的外掛更新不會產生提示,也不會產生工具呼叫, 所以代理程式自己的介面,沒能給開發者任何可以抓住的東西。Air 的論點是,這個攻擊跑在 系統中你沒有理由去盯著的那一塊。
工作區把盯著這件事搬到別處。安全引擎把每一個決定寫成你 Mac 上 Security Timeline
視窗裡的一列:防火牆判決(evil.example:443 tcp — blocked)、Guardrails 的執行
(DELETE api.github.com/repos/… — blocked)、credential brokering,包括紅色的
blocked — exfiltration attempt, VM paused 那一列,外加供應鏈判決與提示注入命中,
都有顏色標示、可以篩選。一個工作階段還可以錄下已執行指令與已連到主機的加密軌跡。
一個沒告訴你就自己更新的外掛,終究還是得發出請求,而每一個請求都要越過一道邊界, 那道邊界在 hypervisor 的另一側留著自己的記錄。
先確認你的代理程式版本
Claude Code 2.1.179 與 Codex 0.146.0 含有這項修正。如果你用 Gemini CLI,不會有 修正。如果你用 Copilot,目前還沒有。
能關的地方就關掉外掛自動更新
零點擊這個性質來自背景更新程式。手動更新會把人重新放回步驟五的迴圈裡。
在 CI 裡自己驗證簽出
在釘選的簽出之後執行 git rev-parse HEAD,拿來和釘選比對,就是廠商不得不補上的
那一行檢查。凡是你用 SHA 克隆的地方,它都管用。
假設有人會贏得簽出
釘選、審查、市集審核,在這裡全都守住了,攻擊還是打進來。接下來的結果,就取決於 贏得簽出的那段程式碼構得著什麼。
Plugin4Shell 用同樣的條件抓住謹慎的開發者和粗心的開發者。裝了當紅熱門那位,下場並 不比讀了原始碼、釘住雜湊、把釘選記下來的那位更糟。內容定址本該保護第二位開發者, 它卻沒有做到,因為 git 是透過一種查找來解析這個位址,而那種查找也把同樣的字元拿去 指別的東西。
繼續釘選。然後假設:一個建立在查找或名稱比對之上的控制,總有一天會在你睡著的時候 被交上錯誤的答案。為那個早晨做設計:讓代理程式跑在一個沒有任何值得拿走的金鑰的 地方,並把它做過什麼的記錄,留在程式碼構不著的邊界另一側。