外观
LiteLLM 网关鉴权与控制面组合失陷
摘要
OffGuard 对 LiteLLM 网关的身份、MCP、Guardrail 和透传请求路径开展审查。研究报告的关键链包括:MCP 鉴权异常被捕获后降级为空身份对象,任意格式正确的 Authorization 头可能获得会话;生产 Guardrail 创建接口动态编译并执行代码,却没有沿用测试接口的过滤;删除数据库中的 Guardrail 后,内存回调仍持续运行;透传请求还可访问云元数据服务。
互联网非破坏性扫描覆盖 3,000 余个实例,其中 191 个(6.2%)无认证、103 个(3.4%)接受文档示例默认密钥,330 个对无效 Bearer 值表现出 MCP 绕过特征。扫描没有读取第三方数据或执行工具,因此这些数字是配置与鉴权暴露,不是已确认失陷数量。
为什么一个模型代理会成为高价值控制面
LiteLLM 不只是把 OpenAI 格式转换到多个模型提供商。典型部署还集中保存:
- OpenAI、Anthropic、Bedrock 等上游提供商密钥。
- 每一次提示词和模型响应。
- 连接数据库、GitHub、Slack 等系统的 MCP 工具。
- 云 IAM Role 和代理所在网络的访问能力。
- 管理 UI、预算、用户、JWT 和可执行 Guardrail 配置。
材料指出一个 LITELLM_MASTER_KEY 同时控制 API 认证、管理 UI 和 HS256 JWT 签名;文档示例长期使用 sk-1234。共享主密钥不是四条发现的必要条件,却会显著扩大任何一条认证或配置缺陷的影响。
四条独立路径不能合并成一个“万能漏洞”
| 路径 | 最先失效的控制 | 前置条件 | 直接后果 |
|---|---|---|---|
| MCP 鉴权降级 | 认证异常被转换为空身份 | MCP 路由可达 | 未授权会话与工具访问 |
| Guardrail 生产接口 | 测试与生产验证路径不一致 | 可创建 Guardrail;未设主密钥时条件更宽 | 网关进程执行 Python |
| Ghost Guardrail | 数据库与内存状态不一致 | 已存在可执行 Guardrail | 删除后继续执行直到进程重启 |
| 透传请求 SSRF | 任意目标和敏感请求头未受限 | 管理员可配置透传路由 | 访问内网或云元数据 |
实际攻击可以串联这些路径,但每项修复和验证对象不同。仅修复 MCP 并不能清除内存中的 Guardrail,也不能阻止管理员创建危险透传路由。
路径一:MCP OAuth2 透传把失败认证降级为空身份
相关代码位于 user_api_key_auth_mcp.py。路由先根据请求头判断是否存在 OAuth2 透传头,再调用常规 user_api_key_auth。问题出在异常处理:当认证抛出 401 或 403 时,代码没有立即返回,而是构造一个空的 UserAPIKeyAuth() 对象继续执行。
概念化代码如下:
python
if oauth2_headers:
try:
validated = await user_api_key_auth(...)
except HTTPException as exc:
if exc.status_code in (401, 403):
validated = UserAPIKeyAuth() # 旧逻辑:继续执行任意格式正确的 Authorization 头都会让请求进入 oauth2_headers 分支。演讲使用一个无效 Bearer 值即可收到 HTTP 200 和 mcp-session-id;完全不带头时反而进入另一条错误路径并返回 500。会话建立后,客户端可调用 tools/list。若服务配置 allow_all_keys,空身份可能看到所有工具。
这不是 OAuth2 协议缺陷,而是应用把“认证失败”和“允许匿名”表示为同一种空对象。修复必须确保 401/403 立即终止,不能只检查请求是否存在 Authorization 头。
公告与版本
官方 GitHub Advisory 将该问题标为 GHSA-7488-6r32-c95q、CVE-2026-59822,影响 < 1.84.0,1.84.0 修复,CVSS 8.8。演讲末页把两个 GHSA 与问题名称的对应关系写反;本文按官方公告映射,而不是照抄幻灯片表格。
路径二:Guardrail 测试接口安全,生产接口却执行原始代码
LiteLLM 支持 Custom Code Guardrail,在每次请求上执行 apply_guardrail()。研究者比较了两个入口:
POST /guardrails/test会检查禁止模式,并移除可用 builtins。- 生产
POST /guardrails进入guardrail_hooks/custom_code/custom_code_guardrail.py,旧实现对用户代码执行compile(..., "exec")和exec,保留完整 builtins。
因此,在测试页被拒绝的 Python 代码可以通过生产创建接口保存并在每个代理请求上执行。默认 Docker 部署又常以 root 运行,使进程级代码执行具有较高影响。
权限条件需要精确描述:官方公告把问题定为低危,因为通常需要高权限用户创建 Guardrail;演讲指出,LITELLM_MASTER_KEY 未设置时,部分部署会自动把调用者赋为 PROXY_ADMIN,从而显著降低前置权限。不能把后一配置外推为所有默认实例。
公告与版本
官方公告为 GHSA-72m8-9m7m-h278、CVE-2026-59821,影响 < 1.82.0,在 1.82.0-stable 修复;官方 CVSS 为 2.1。较低评分反映默认前置权限,不代表动态 exec 本身没有代码执行能力。
路径三:Ghost Guardrail 已从数据库删除,内存回调仍在运行
Guardrail 创建后同时存在于数据库配置和 Python 进程的 callback 列表。旧删除接口只移除数据库行并返回成功,没有同步卸载内存对象。管理 UI 和列表接口都看不到该规则,但每个后续请求仍会触发它,直到相关 worker 进程重启。
这是一种配置面持久化:
text
数据库:已删除
管理 API:查询不到
运行时 callback:仍存在并继续执行若部署有多个 worker,重启或删除操作还可能只影响部分进程。验证不能只看 HTTP 200 和数据库记录,应比较配置期望状态与每个 worker 的实际 callback 摘要。演讲称该问题已修复,但没有给出单独公告编号。
路径四:透传路由把网关变成可编程请求客户端
LiteLLM 允许管理员创建 Pass-through Endpoint,把进入代理的请求转发到自定义上游。研究版本没有默认拒绝回环、RFC 1918 私网或 169.254.169.254 等云元数据地址,也没有完整限制转发头。
AWS IMDSv2 通常要求先取得短期 token,再在后续请求头中提交。LiteLLM 会处理带 x-pass- 前缀的自定义头并移除前缀后转发,这使管理者可能构造多阶段元数据请求。本站不公开真实元数据地址、头组合或两步请求,只保留防护结论:目标地址和转发头必须按最终连接进行限制,不能把“仅管理员可配置”当作唯一 SSRF 控制。
项目曾把这一行为视为透传功能的预期用途。即便如此,托管或多租户部署仍应把“创建任意上游”视为内网访问权限,而不是普通模型配置权限。
互联网非破坏性扫描
研究者先识别 3,000 余个公开 LiteLLM 实例,再用不读取数据、不调用工具的方式检查认证状态。材料报告:
| 观察 | 数量 | 占扫描实例比例 | 正确含义 |
|---|---|---|---|
| 无认证即可访问 | 191 | 6.2% | 配置暴露,不代表已执行工具 |
接受示例默认密钥 sk-1234 | 103 | 3.4% | 默认密钥未更换 |
| 无效 Bearer 值呈现 MCP 绕过特征 | 330 | 未给出统一分母口径 | 鉴权行为匹配,不代表后端权限相同 |
作者还称两个实例暴露约 25 个 MCP 工具且不需认证。扫描遵循非破坏性原则,没有调用这些工具或读取第三方数据。比例受扫描时间、识别方式、端口和重复实例影响,不能代表全球全部部署。
从暴露面到真实利用:90 天蜜罐遥测
Wiz 随后公布的 90 天蜜罐遥测把部分风险从扫描特征推进到真实攻击活动。研究者观察到攻击者用单字符 Bearer 值探测 LiteLLM,行为与 CVE-2026-59822 的认证失败降级一致;还观察到针对 MCP Server 测试接口命令注入漏洞 CVE-2026-42271 的利用,恶意命令在返回有效 MCP 握手的同时启动挖矿程序。后者是独立于 Guardrail 动态执行路径的输入校验缺陷,并可与 Starlette Host 头校验绕过 CVE-2026-48710 组合成未认证远程代码执行,因此验证范围不能只覆盖本文前述四条 OffGuard 路径。
入侵后的操作也体现了 AI 网关的特殊资产集中风险:蜜罐记录到攻击者直接查询 LiteLLM 运行进程的 Python 模块状态以读取主密钥,并枚举后端模型,再决定窃取提供商密钥或滥用推理额度。同一批遥测还在 LangChain、Flowise、OpenWebUI 和 Node-RED 环境中观察到以带外 DNS 回调确认工具执行的盲提示词注入,以及按 AI 框架目录和进程特征伪装挖矿程序的行为。这些观测支持“网关失陷会扩展到凭据、工具和云权限”的判断,但报告没有披露各类事件的完整分母、去重方法或受害环境代表性,不能据此估算互联网总体利用率,也不能证明每个公开实例都遭到入侵。
去武器化 PoC:本地四项回归
认证失败必须关闭会话
http
GET /mock/mcp/session HTTP/1.1
Host: gateway.example.invalid
Authorization: Bearer invalid-test-token
Expected: 401
Assert: session_created == false
Assert: tool_calls == 0测试与生产必须使用同一 Guardrail 编译器
text
fixture = "return 'DRY_RUN'"
test_digest = validate_and_compile(fixture, policy="restricted-v2")
create_digest = create_guardrail(fixture, policy="restricted-v2")
assert test_digest == create_digest
assert full_builtins_available == false删除必须卸载所有 worker 的内存状态
text
delete_guardrail("dry-run-id")
for worker in cluster.workers:
assert "dry-run-id" not in worker.callback_ids
assert database.lookup("dry-run-id") is NoneSSRF 只连本地 Mock,且最终地址必须拒绝
text
route.target = "http://metadata.mock.invalid/"
dns.resolve("metadata.mock.invalid") = "127.0.0.1"
assert create_route(route) == POLICY_DENIED
assert outbound_connection_count == 0这些测试只用于自有环境。原始研究中的动态代码命令、真实云元数据地址、敏感转发头和第三方工具调用均不纳入本站复现材料。
防护措施与验证方法
- 认证失败必须立即终止请求;匿名身份使用独立类型,并在会话、工具和预算层默认无权限。
- 管理 UI、模型数据面、MCP、Guardrail 和 JWT 签名使用不同密钥与角色;弃用示例默认密钥并定期轮换。
- 未设置主密钥时应拒绝启动生产服务,而不是自动授予管理角色。
- 禁止上传任意 Python Guardrail;优先使用受限策略语言或签名插件。测试、创建、更新和恢复必须走同一验证器。
- Guardrail 更新与删除以“数据库 + 全部 worker 内存”原子提交;持续比较期望配置摘要与运行时 callback 摘要。
- 透传目标默认拒绝回环、私网、链路本地和云元数据地址;解析 DNS、跟随重定向及建立连接时重复校验最终 IP。
- 维护可转发请求头允许列表,禁止调用者通过前缀恢复云元数据 token、Host、代理认证等敏感头。
- 网关进程以低权限用户运行,模型密钥、MCP 凭据和云角色按功能拆分,避免单进程持有所有资产。
- MCP Server 测试接口不得把配置中的命令字段直接交给子进程;测试任务应使用固定可执行文件允许列表、低权限隔离环境和无外联策略,并监控 AI 服务派生 shell 或异常子进程的行为。
关键结果与实际影响
研究说明 LiteLLM 的高风险不来自单个“AI 特有漏洞”,而来自多个控制面集中在同一主密钥、进程和网络位置。认证降级可暴露 MCP,生产 Guardrail 与 MCP 测试接口的输入校验缺陷可执行代码,内存状态可躲过删除,透传路由又能访问内网。90 天蜜罐遥测进一步确认,攻击者已经探测单字符 Bearer 绕过、利用 MCP 测试接口,并从运行时内存定位网关主密钥。只有在具体功能启用、版本易受影响且权限条件满足时这些路径才可串联;扫描特征或蜜罐观测都不能证明所有公开实例已失陷。
局限与待验证问题
会议材料与官方 GHSA 能支持主要机制和修复版本,但默认配置、托管服务和旧配置迁移仍需按部署核对。公开扫描只检查认证行为,无法确认后端工具或云权限;蜜罐遥测证明特定攻击路径已在观察环境中出现,却缺少可用于总体流行率推断的事件分母和采样代表性。还需验证多 worker、滚动升级、重定向、DNS 重绑定、Guardrail 恢复备份和 MCP 测试任务隔离是否受到同一策略覆盖。