跳到正文
DEV Community· Gabriel Anhaia·· 2 小時前精選AI 評分77

刪除檔案未能撤銷金鑰:LiteLLM 1.107.0 如何被植入惡意軟件並發佈

Deleting the File Didn't Revoke the Key: How LiteLLM 1.107.0 Shipped Malware

AI 導讀

LiteLLM 的 litellm==1.107.0 套件及 Docker 映像於 10 月 21 日遭攻擊者植入惡意軟件並發佈。攻擊始於 5 月 27 日公開 Git 歷史中的管理金鑰;檔案雖約三小時後刪除,金鑰仍有效逾四個月,受感染產物約公開兩小時,Wiz 估計該時段有約 49,000 次下載。

推薦理由

文章梳理未撤銷管理金鑰如何一路導致套件遭植入惡意軟件,並整理先撤銷憑證、限制自動化權限等防護做法。

正文 · 繁體中文

2026 年 5 月 27 日 18:53 UTC,一個名為 opencode.json 的檔案被放進公開的 LiteLLM-Labs/lite-harness 儲存庫。檔案內有 LiteLLM Agent Platform 內部部署所用的管理員金鑰。約三小時後,後續一次提交刪除了該檔案。按 LiteLLM 自己的説法,「沒有人跟進撤銷管理員金鑰」(LiteLLM,10 月 23 日)。

10 月 21 日,該金鑰被證實是整條攻擊鏈的起點,最終導致惡意 litellm==1.107.0 出現在 PyPI,而 Docker latest 標籤則指向遭入侵的映像檔(LiteLLM,10 月 21 日)。

以下事件的所有資料均來自 LiteLLM 的兩份事後檢討報告及 10 月 30 日的跟進説明,另有一項來自 Wiz 的下載量估算。

攻擊鏈逐步經過的環節

除另有註明外,所有時間均為 UTC,資料來自攻擊鏈事後檢討報告。

  1. 5 月 27 日。 opencode.json 提交。檔案於三小時後被刪除,但該提交一直留在公開歷史記錄中,直至 10 月 22 日;金鑰亦一直有效。
  2. 10 月 10 日,21:13 至 21:28。 攻擊者使用該金鑰,向 Agent Platform API 發出經驗證的請求。21:27:10,其中一次呼叫 GET /api/v1/managed_agents/agents?limit=100 返回所有受管理 Agent 的設定。其中一份設定內有一名 LiteLLM GitHub 組織兩位擁有者之一的 GitHub CLI OAuth 權杖。
  3. 該權杖為何會在那裏。 平台會建立拉取請求,因此需要 GitHub 權杖。但系統獲提供的是該擁有者的 GitHub CLI 權杖,而不是僅限於相關儲存庫的個人存取權杖。事後檢討報告指出:「GitHub CLI 權杖沒有設定範圍」,而攻擊者利用這個權杖「取得了我們 GitHub 組織的擁有者權限」。
  4. 10 月 10 日,22:11 至 22:14。 兩個臨時分支 ci-tmp-a351cc 和 ci-tmp-be33e9 執行工作流程,將儲存庫機密傳送到 webhook.site 端點。竊取所得包括另外兩名維護者的權杖。兩次工作流程執行記錄均在三分鐘內被刪除。
  5. 10 月 21 日,10:08。 分支保護機制拒絕向私有發佈儲存庫推送變更。五秒後,擁有者權杖仍成功推送變更。該變更在發佈工作流程中加入 source_owner 輸入參數,令工作流程可從任何組織建置。
  6. 10:46 至 11:11。 測試部署針對外觀相似的 Berri-AI/litellm 執行,而非 BerriAI/litellm。11:11:18,第二個被盜權杖核准了正式環境關卡。LiteLLM 不久前放寬了該關卡,令「任何兩位工程師」都可以觸發並核准發佈。
  7. 11:39 至 11:47。 七個惡意 wheel 套件被上傳至 PyPI。映像檔則以 v1.107.0、latest、main-latest 和 main-stable 標籤推送至 Docker Hub 和 GHCR(LiteLLM,10 月 21 日)。
  8. 11:53 起。 第三個被盜權杖刪除了工作流程執行記錄。PyPI 於 13:23 前已移除該次發佈,最後一批 Docker Hub 標籤則於 14:20 改為指向乾淨的映像檔(LiteLLM,10 月 21 日)。

受影響的檔案在外流期間持續可用「約兩小時」(LiteLLM,10 月 23 日)。該版本「包含經編譯的惡意軟件,會竊取憑證並載入遠端程式碼執行工具」,而 GitHub 上官方的 v1.107.0 原始碼標籤從未包含這些惡意檔案(LiteLLM,10 月 21 日)。Wiz 引述 ClickPy 數據,估計該時段 1.107.0 的下載量約為 49,000(Security Boulevard)。

LiteLLM 首篇貼文對 token 來源的説法有誤。它懷疑這些 token 來自內部部署的記錄。10 月 23 日的貼文指出,請求記錄顯示第一個 token 來自另一項服務,即步驟 2 的 Agent Platform(LiteLLM,10 月 23 日)。上文的步驟依照更正後的説法編寫。

A single small key at the start of a long chain whose links pass through a server, a token, a gear and end at a sealed shipping box
從五月一把未撤銷的金鑰,到十月一個遭污染的版本。

為何刪除檔案毫無作用

刪除檔案的提交會新增一個快照。含有金鑰的舊快照仍可透過其雜湊值,從期間建立的每個 clone 和 fork 存取。GitHub 關於移除敏感資料的指南列出了即使重寫歷史記錄並強制推送後,舊提交仍會留存的位置:clone、fork、可透過 SHA 存取的快取檢視頁面,以及引用該提交的拉取要求。

因此,同一份指南將一個步驟列為首要事項:撤銷及/或輪替金鑰。指南指出,金鑰失效後,重寫歷史記錄「可能沒有必要」。已撤銷的金鑰留在公開歷史記錄中並無害處。檔案雖已刪除、金鑰卻仍有效,仍然是有效金鑰;而這把金鑰持續有效超過四個月。

編碼代理令這種情況更容易被忽略。代理工作階段可能在一個回合新增設定檔,然後在之後的回合將它移除。拉取要求的最終差異會抵銷這兩項變更,因此只查看該差異的審閲者根本不會看到金鑰。LiteLLM 沒有説明是誰或甚麼建立了五月的提交,本文亦不假設是代理所為。重點是,無論如何,只審閲最終差異都會漏掉這把金鑰。

不要把金鑰寫進檔案

opencode.json 是 OpenCode 的專案設定檔。OpenCode 會在目前目錄中尋找該檔案,並向上搜尋至最近的 Git 根目錄,因此按設計它會與你的程式碼放在一起(OpenCode 文件)。同一份文件亦提供了避免把金鑰寫進檔案的方法:{env:VAR} 和 {file:path} 替代方式。以下是文件中的例子:

{
  "$schema": "https://opencode.ai/config.json",
  "model": "{env:OPENCODE_MODEL}",
  "provider": {
    "anthropic": {
      "models": {},
      "options": {
        "apiKey": "{env:ANTHROPIC_API_KEY}"
      }
    }
  }
}

該檔案可以提交至版本庫、接受審閲,亦可查看差異。金鑰存放在你的環境中,或存放在 ~ 下、不會納入版本庫的檔案中。其他代理的設定檔各有引用秘密值的方法;在代理代你寫入明文金鑰前,請先檢查你所用的設定檔。

在歷史記錄中找出金鑰,然後先撤銷

git log 可在每個提交中搜尋指定字串。-S 會找出字串出現次數有變的提交,因此能同時找出加入金鑰和移除金鑰的提交。-G 會以正規表示式比對新增或刪除的行。-p 會輸出修補內容,而 --all 會檢查本機的每個引用,而不只是目前分支。

# commits that added or removed the string, any ref
git log --all --oneline -S 'sk-'

# same commits, with the lines themselves
git log --all -p -S 'sk-'

# looser: added or removed lines matching a regex
git log --all -p -G '(api|API)_?(key|KEY)'

# files that were deleted, and in which commit
git log --all --diff-filter=D --name-only --oneline

將 sk- 換成你的供應商所用的前綴。最後一條指令會找出 LiteLLM 這類情況:設定檔存在於歷史記錄中,卻不在檔案樹中。

找到問題後,應按以下次序處理:

  1. 首先,在供應商端撤銷或輪替金鑰。
  2. 檢查供應商的記錄,查看自該提交日期起有否使用該金鑰。
  3. 之後才決定是否重寫歷史記錄,並要知道 clone 和 fork 仍會保留舊提交(GitHub)。

LiteLLM 在 10 月 30 日發佈的貼文,把第 1 步變成一套流程:「機密資訊掃描必須自動開立工單,只有確認已撤銷後才可關閉工單」(LiteLLM,10 月 30 日)。

A stack of translucent file layers with a gap where a file was removed from the top layer, while a magnifier shows a key still sitting in a lower layer
刪除檔案只是多加一層防護,金鑰仍在下一層。

限制代理程式工作階段可使用的 gh 權杖範圍

這條鏈的第 3 步,令外洩的平台金鑰演變成對 GitHub 組織的控制權。一個只需開啟拉取請求的服務,卻持有組織擁有者的 CLI 登入資料。

同樣的情況也會以較小規模出現在手提電腦上。在終端機中執行的編碼代理程式,會以你的身分運作。如果你使用 gh 登入,代理程式便可執行 gh auth token,而該指令會「輸出你帳戶的驗證權杖」。如果沒有可用的憑證儲存區,gh 便會退而把權杖寫入純文字檔(gh 文件)。

gh 會先讀取 GH_TOKEN:gh 環境變數文件指出,它「優先於先前儲存的憑證」。因此,代理程式工作階段應匯出一個僅限單一儲存庫、只具備工作所需權限並設有到期日的細粒度個人存取權杖:

# in the shell you start the agent from
export GH_TOKEN="$(cat ~/.config/gh-agent/app-repo.token)"
gh auth status

有兩點需要注意。這只會改變 gh 使用哪個權杖,不會限制以你的身分執行的程序可讀取哪些內容;使用沙盒或獨立的作業系統用戶,才是更穩固的隔離界線。另外,對於代表組織執行的自動化流程,GitHub 建議使用 GitHub App,而非任何個人權杖(GitHub)。

LiteLLM 得出的規則也一樣:「自動化流程會使用 GitHub App 進行驗證,或使用權限範圍最小、設有到期日的細粒度權杖」,而日常工作中使用代理程式的人不會獲授擁有者或管理員角色(LiteLLM,10 月 30 日)。同一篇貼文指出,3 月後加入的發布控制措施按設計運作,但未能完全阻止這次事件;貼文亦直言原因:「任何管線控制措施都無法防範組織擁有者身分的攻擊者,因為擁有者本身就擁有整條作業管線。」

Hermes 的適用範圍與限制

我開發 Hermes IDE,因此請將這點納入考量。Hermes 會執行 Claude Code、Codex、OpenCode 和其他代理程式,每個代理程式各自在獨立的終端機和工作樹中執行。Hermes 從不索取權杖:代理程式各自透過 CLI 登入(README)。這也代表 Hermes 不會限制代理程式可存取的資源範圍。上文的 GH_TOKEN 步驟仍須由你自行完成。

它改變的是審查方式。每個代理程式回合都會儲存下來,而 Review Desk(⌘G;Windows 和 Linux 上按 Ctrl+Shift+G)可按回合或檔案查看變更,並附有風險標記(README)。之前提到的先新增後刪除模式,在這裏不會相互抵銷:即使之後的回合刪除了設定檔,寫入該檔案的回合仍會顯示自己的差異內容。

The Hermes Review Desk in By turn view, listing two turns of a Claude Code task with the first turn's diff open
Claude Code 在 orders-api 中執行工作時,Review Desk(⌘G)的「按回合檢視」畫面:列出兩個回合,第 1 回合中 src/db.ts 的差異已開啟,並顯示已變更檔案清單,以及根據各回合草擬的提交訊息。

如果這類回合顯示了金鑰,審查便已完成任務;下一步仍是按下服務提供者的撤銷按鈕。5 月發生的情況,就是事後刪除檔案。

Hermes 免費,原始碼可供查閲(BSL 1.1),可在 macOS、Windows 和 Linux 上執行,亦與任何代理程式供應商沒有關聯。下載,或到 GitHub 閲讀程式碼。


如果這篇內容對你有幫助

LiteLLM 這條事件鏈既是安全問題,也是設計問題:一組憑證所能觸及的範圍,遠超持有該憑證的服務實際所需,而後續每一道關卡都信任這組憑證。System Design Pocket Guide: Fundamentals 是我撰寫的一篇短文,介紹這些系統的核心組成部分。這是八本精簡版 Pocket Guides 之一,內容涵蓋 Agent、RAG、提示詞、可觀測性、事件、資料庫及系統設計:完整系列在此。

System Design Pocket Guide: Fundamentals — Core Building Blocks for Scalable Systems

來源:DEV Community · dev.to