代理的權杖是組織擁有者的
10 月 23 日,LiteLLM 公布了遭汙染的 1.107.0 版本的根本原因。第一個被竊的憑證並非來自日誌,而是來自 LiteLLM 自己的代理平台:它的 API 會回傳雲端代理用來開 pull request 的 GitHub 權杖,而那是組織擁有者的權杖,權限沒有收窄。這個 API 的金鑰在一份公開設定檔裡放了四個半月。在 Bromure Agentic Coding 中,開 pull request 的代理拿到的是替身,真正的權杖加密留在 Mac 上,沒有任何 API 會回傳它。
LiteLLM 的代理需要一個 GitHub 權杖來開 pull request。它們拿到的,是屬於組織擁有者的權杖。一次使用外洩金鑰的 API 呼叫,就把它取了回來。
10 月 23 日,LiteLLM 公布了其 1.107.0 版本背後的攻擊鏈,也就是 10 月 21 日夾帶憑證竊取程式出貨的那個版本。這篇文章自稱「權威的唯一事實來源」,並修正了 LiteLLM 最初的推論。被竊的 GitHub 權杖並非來自記錄下來的模型流量。第一個權杖來自 LiteLLM 為自家程式碼代理運行的一項實驗性服務。
我們在那個版本才發布兩小時中報導過這次發布本身,當時依循的是最初的推論。最終報告的起點不同:攻擊者拿到的第一個憑證,屬於那些代理。
握有擁有者權杖的代理平台
用 LiteLLM 的話說,LiteLLM Agent Platform 是「一個讓自架雲端代理向 GitHub 儲存庫提交 PR 的實驗性專案」。為了開 pull request,平台持有一個 GitHub 權杖。這個權杖「沒有綁定服務帳號,權限也沒有收窄」。它是組織兩位擁有者之一的 GitHub CLI OAuth 權杖。
報告中的時間軸(UTC):
- 5 月 27 日 18:53。 一個
opencode.json檔案被提交到公開儲存庫 LiteLLM-Labs/lite-harness。那是一個代理執行框架的設定檔,裡面有 Agent Platform 某個部署的主金鑰。團隊發現後,用 force-push 蓋掉那次提交,並把外洩的金鑰標記為待撤銷。「主金鑰的撤銷並未執行。」 - 10 月 10 日 21:27。 攻擊者從 69.48.229.143 用這把金鑰呼叫平台的 API。
GET /api/v1/managed_agents/agents?limit=100回傳了每個受管代理的設定,連帶擁有者的權杖。 - 10 月 10 日 22:11。 攻擊者用擁有者的權杖,把一個拋棄式分支
ci-tmp-a351cc推到 BerriAI/litellm。它的工作流程把六個儲存庫機密送到 webhook.site。其中的GH_TOKEN,是第二位維護者的 GitHub CLI 權杖。二十四秒後,BerriAI/litellm-auto 裡的第二個分支把第三位維護者的權杖送到同一個端點。兩次執行都在三分鐘內被刪除。 - 10 月 12 日。 私有儲存庫上又有其他工作流程,把能碰到的每個儲存庫機密都送了出去。
- 10 月 21 日 10:08。 以第二位維護者權杖推送到發布儲存庫的變更,被分支保護擋下。五秒後,擁有者的權杖關閉保護並 force-push 這項變更,讓發布工作流程指向仿冒的 Berri-AI/litellm。
- 10 月 21 日 11:11。 第二位維護者的權杖核准了正式環境關卡。原本有條規則要求由綁定在 LiteLLM 辦公室某台裝置上的帳號核准,那一週被放寬了,於是任意兩位工程師都能發布。
- 10 月 21 日 11:39 至 11:46。 七個 wheel 上了 PyPI,接著是 Docker Hub 和 GHCR 的映像檔。
- 10 月 21 日 11:53。 第三位維護者的權杖刪除了發布執行的紀錄。12:24,擁有者的權杖用 force-push 把 main 恢復乾淨。
LiteLLM 自己的總結是:「這起事件有力地說明了,沒有輪替可能已外洩的金鑰會帶來什麼風險。」該公司現在正在撤銷與每個內部系統、每個供應商平台相關的所有權杖。
每支代理隊伍都握有這樣的權杖
LiteLLM 有一支能幹的資安團隊,它的配置也很常見。開 pull request 的代理需要一個能推送的權杖。最順手的權杖就是你自己的 gh 登入,而 LiteLLM 的代理拿到的正是這個。
接著,權杖得放在代理讀得到的地方:環境變數、設定檔、某個用來設定代理的 API 背後的資料庫列。誰能讀那個地方,誰就拿到權杖。這次,一把外洩過一次、從未輪替的 API 金鑰就夠了。
GitHub CLI 權杖讓情況更糟。LiteLLM 指出,這些權杖「是在通過 2FA 之後核發的,不受 GitHub 針對組織或儲存庫敏感操作的『超級使用者』2FA 重新驗證流程約束」。有了一個,攻擊者就能關掉分支保護、force-push、核准發布,再抹掉證據。
在你自己的配置裡找找同樣的零件:代理環境裡的 GH_TOKEN、含有 bearer 權杖的 MCP 設定、某天會有人提交出去的執行框架設定。LiteLLM 的 opencode.json 裡不只有平台金鑰,還有它的 GitHub、Linear、Slack MCP 伺服器的 bearer 權杖,以及模型閘道的金鑰,全都是明文。
同一支隊伍放在 Bromure 機器上
Bromure Agentic Coding 在你的 Mac 上以 Linux VM 執行 Claude Code、Codex、Grok、Kimi 和 omp。它的自動化做的事和 LiteLLM 的代理平台一樣:代理依排程或在出現新的 GitHub issue 時啟動,在全新的 worktree 裡工作,然後推送分支或開 pull request。拿 LiteLLM 的攻擊鏈對一台執行這些自動化的機器跑一遍。
代理拿到的是替身
在機器的 Credentials 中加入 GitHub 權杖。開機時,Bromure 會在 VM 裡寫入一個假的:長度正確的 ghp_ 值,放在 GH_TOKEN、gh 的設定和 ~/.git-credentials 裡。代理的 gh pr create 和 git push 照常運作。當請求離開 VM 前往 github.com 時,Mac 上的代理伺服器會把假的換成真正的權杖,而且只針對那個主機。
真正的權杖加密存放在 Mac 上,加密金鑰保存在 macOS 鑰匙圈裡。它從不寫入 VM,不在任何檔案,也不在任何變數。手冊這樣描述主機與 VM 之間的通道:「VM 沒有任何請求可以把真正的權杖要回來。」
所以,打開 LiteLLM 組織的那一步,在這裡沒有東西可回傳。把代理的環境傾印出來、讀它的設定、透過 API 列出每個自動化:裡面的 GitHub 值全是替身。把它貼進你自己的 gh,GitHub 會拒絕,因為只有那台機器的代理伺服器知道它代表什麼。
提交出去的設定也只有替身
這次外洩始於公開儲存庫中的一份執行框架設定。在 Bromure 機器上,代理讀取的設定檔都是用替身寫成的。在機器的 MCP 面板設定的 MCP 伺服器 bearer 權杖,以 brm-mcp_… 值送到代理手上,在發往該伺服器主機的請求中於傳輸途中替換。在 Preferences › Models 設定的模型金鑰從不進入 VM;代理看到的是符合供應商格式的假值。其他任何 API 金鑰放在 Other API keys,加上主機篩選,VM 會在你指定的變數裡拿到一個 brm_… 值。
當代理不小心提交了自己的設定,或開發者把它複製到示範儲存庫,檔案裡帶的都是替身。裡面沒有任何東西能在那台 Mac 以外使用,所以你會不會被入侵,不取決於一次 force-push,也不取決於一份沒人做完的撤銷清單。
沒有平台 API 可呼叫
LiteLLM 的攻擊者是從網際網路連到平台 API 的。Bromure 的自動化在你的 Mac 上執行。用手冊的話說:「沒有雲端服務,也沒有對內的網路攻擊面。」GitHub 和 Linear 的觸發條件由 Mac 每三分鐘以機器儲存的權杖輪詢一次,那個權杖留在主機上。沒有 webhook 端點,沒有主金鑰,也沒有外洩金鑰能打開的 /managed_agents 路由。
被偷的替身會觸發警報
10 月 10 日的工作流程把機密送到 webhook.site。想像同一招用在 Bromure 機器上:一個被汙染的相依套件,或一段經由提示注入塞進來的指令,讀取 GH_TOKEN 並把它送到 webhook.site。
代理伺服器會檢查每個對外請求的標頭和內文,找出被送往非其簽發主機的替身。一個要送往 webhook.site 的 ghp_ 替身,會在任何位元組送出前被 HTTP 451 擋下。VM 隨即暫停,Mac 上跳出警示 This environment may have been compromised,列出權杖預覽、憑證名稱、它所屬的主機,以及它被送往的主機。你可以關閉機器,或保存它的磁碟和家目錄以供調查。
若機器的沙箱政策把 Unmatched traffic 設為 Deny,webhook.site 根本連不到,這次嘗試會在 Security Timeline 中顯示為紅色的一列。
推送會等你
新的 Bromure 機器啟動時,GitHub 的寫入政策是 Prompt before write。讀取直接放行。git push 或任何對 github.com 的寫入,都會停在你 Mac 上的對話框:
Allow write on "GitHub" from workspace "release-bot"?
POST /repos/<owner>/<repo>/git/refs
攻擊者的每一步都是寫入:ci-tmp- 分支、修改發布工作流程、對 main 的 force-push、刪除執行紀錄。從 Bromure 機器發出的話,每一步都會停在這個對話框。Allow once 只放行那一次寫入,下一次推送會再問一次。force-push 在傳輸上仍是一次推送,Prompt before write 一樣會攔。
無人值守的自動化沒辦法等人點按,所以要給它剛好夠用的最窄權杖,並套用為那台機器設定的政策。你加到 release-bot 機器上的權杖,可以是只限單一儲存庫的細粒度權杖。你自己那個對一切都有擁有者權限的 gh 登入,沒有理由出現在那台機器上。
紀錄在你的 Mac 上
LiteLLM 在 GitHub 的稽核日誌裡比對權杖雜湊,才查出哪個被竊權杖做了什麼。在 Bromure 機器上,每次在傳輸途中替換憑證,都會在 Security Timeline 留下一列 Credential brokering,附上遮罩後的權杖和主機。你拒絕的每次寫入,都是一列紅色的 Guardrails,附上方法和路徑。在已註冊到 bromure.io 的 Mac 上,同樣的事件也會送到你的組織,所以從意料之外的機器使用的權杖會自己浮現,不必有人翻遍稽核日誌。
代理拿到的是替身
GH_TOKEN、gh 的設定和 ~/.git-credentials 裡放的是 ghp_ 假值。真正的權杖在 Mac 上換入,而且只針對 GitHub 的主機。
沒有 API 會發放權杖
自動化在你的 Mac 上執行,沒有對內攻擊面。VM 能呼叫的任何東西,都不會回傳真正的憑證。
外洩會讓機器暫停
替身只要被送往自己主機以外的地方,就會被 451 擋下,VM 也會暫停,直到你做出決定。
寫入前先詢問
新機器在每次推送、每次寫入 GitHub 之前都會詢問,包括 force-push,並把確切的請求顯示在畫面上。
根本不要把權杖交給代理
LiteLLM 正在撤銷所有權杖,並將在下週公布流程上的改變。輪替、收窄權限、服務帳號都有幫助。但每一種做法仍會把一個能用的權杖留在代理讀得到的某處,而那正是攻擊者在 10 月 10 日來找的東西。
在 Bromure 機器上,代理用來工作的權杖,只能透過它自己的代理伺服器、對它被簽發的那一個主機生效。代理讀得到的東西,連到代理的攻擊者也讀得到;而在 Bromure 機器上,這些東西裡永遠沒有真正的權杖。
想知道同樣的替身如何對付鎖定程式碼代理檔案的竊取程式,請讀竊取程式打包走的是替身。關於代理留在自己對話紀錄裡的副本,請讀權杖留在了對話紀錄裡。