💡 一句话总结:一个 HTTP Host header 的解析缺陷,通过 Starlette 这个 3.25 亿周下载量的 Python 框架,击穿了从 FastAPI 到 vLLM、从 LiteLLM 到 MCP Server 的整条 AI Agent 认证链。CVE-2026-48710 不是又一个 Web 漏洞——它是 Python AI 基础设施信任模型的一次系统性失败。
事件时间线
| 时间 | 事件 |
|---|---|
| 2026 年 1 月 | 德国安全公司 X41 D-Sec 在 OSTIF 赞助的 vLLM 安全审计中发现该漏洞 |
| 2026 年 1-5 月 | 漏洞在保密期内,X41 与 Starlette 维护团队协调修复 |
| 2026 年 5 月 21 日 | Starlette 1.0.1 补丁发布 |
| 2026 年 5 月 22 日 | CVE-2026-48710 公开披露,X41 同步发布 Semgrep 规则、CodeQL 查询和 badhost.org 在线扫描器 |
| 2026 年 5 月 22-28 日 | 下游项目紧急响应,FastAPI、vLLM、LiteLLM 陆续发布升级公告 |
注意补丁到公开之间只有 1 天。 对运维团队来说这几乎是零缓冲期——你要么在 5 月 21 日当天就跟进了 Starlette 的 patch release,要么就在 5 月 22 日暴露在公开漏洞信息下。
漏洞原理:request.url 的致命信任
ASGI 的两条路径
要理解 BadHost,先要理解 Python ASGI 应用中存在两条路径来源。
第一条是 ASGI scope 中的 path。这是由 ASGI 服务器(如 Uvicorn)从 HTTP 请求行中解析出来的,不受 Host header 影响:
# 这条路径来自 HTTP 请求行 "GET /admin/users HTTP/1.1"
# 无论 Host header 写什么,scope["path"] 始终是 "/admin/users"
scope = {
"type": "http",
"path": "/admin/users",
"headers": [
(b"host", b"evil.com/public?x=#"), # 恶意 Host
],
}
第二条是 Starlette 的 request.url.path。Starlette 的 Request 对象会用 Host header 拼接出完整 URL,然后从这个 URL 中重新解析 path:
from starlette.requests import Request
request = Request(scope)
# Starlette 内部逻辑(简化):
# 1. 从 scope["headers"] 取出 Host header → "evil.com/public?x=#"
# 2. 拼接完整 URL → "http://evil.com/public?x=#/admin/users"
# 3. 解析 URL 的 path 部分 → "/public"(因为 ? 后被当作 query,# 后被当作 fragment)
print(request.url.path) # → "/public" ← 被伪造!
print(scope["path"]) # → "/admin/users" ← 真实路径
攻击向量
攻击者在 Host header 中注入三种字符,分别产生不同效果:
| 注入字符 | Host 值示例 | request.url.path 结果 | 效果 |
|---|---|---|---|
/ | evil.com/public | /public | 路径被替换为 /public |
? | evil.com?fake= | / | 路径被截断为根 |
# | evil.com#fragment | / | 路径后的部分被丢弃 |
这些字符可以组合使用。比如 Host: evil.com/health?x=# 会让 request.url.path 返回 /health,而实际请求路径是 /admin/delete-all-data。
为什么中间件会被绕过
现在看一个典型的基于路径的认证中间件:
class AuthMiddleware:
"""常见的 FastAPI/Starlette 认证中间件写法"""
PUBLIC_PATHS = ["/health", "/docs", "/openapi.json"]
async def __call__(self, scope, receive, send):
if scope["type"] == "http":
request = Request(scope)
# ❌ 危险!读的是 request.url.path
if request.url.path in self.PUBLIC_PATHS:
# 跳过认证
await self.app(scope, receive, send)
return
# 验证 token...
token = request.headers.get("authorization")
if not self.verify_token(token):
# 返回 401
...
攻击者发送:
GET /admin/secret-endpoint HTTP/1.1
Host: evil.com/health
中间件读到 request.url.path 是 /health,判定为公开路径,跳过认证。但 ASGI 路由器用 scope["path"](即 /admin/secret-endpoint)做路由匹配,请求被转发到了受保护的管理端点。认证检查和路由匹配看到的是不同的路径。
安全的写法
修复极其简单——改读 scope["path"]:
class AuthMiddleware:
PUBLIC_PATHS = ["/health", "/docs", "/openapi.json"]
async def __call__(self, scope, receive, send):
if scope["type"] == "http":
# ✅ 安全:直接读 ASGI scope 中的 path
if scope["path"] in self.PUBLIC_PATHS:
await self.app(scope, receive, send)
return
request = Request(scope)
token = request.headers.get("authorization")
if not self.verify_token(token):
...
一行代码的差异。但就是这一行,决定了认证是否形同虚设。
为什么 AI Agent 基础设施特别脆弱
BadHost 在传统 Web 应用中也是高危漏洞,但在 AI Agent 基础设施中,它的破坏力被放大了数倍。原因有三。
1. 直接暴露的部署习惯
传统 Web 应用几乎都部署在 Nginx / Caddy / Cloudflare 后面,反向代理通常会重写 Host header。但 AI 基础设施的部署习惯完全不同:
- vLLM 推理服务:很多团队为了减少延迟,直接暴露 Uvicorn 端口给内网调用方,没有反向代理
- LiteLLM 代理:作为 API 网关,常常被部署在
0.0.0.0:4000直接接收流量 - 开发环境的 FastAPI 服务:
uvicorn main:app --host 0.0.0.0是最常见的启动方式,开发者甚至经常用 ngrok 把它暴露到公网
这种部署习惯意味着恶意 Host header 可以不经任何过滤直接到达 Starlette。
2. 高价值的攻击目标
绕过传统 Web 应用的认证,攻击者拿到的可能是用户数据。但绕过 AI Agent 基础设施的认证,攻击者拿到的是:
- LLM 推理端点的直接访问权:可以白嫖 GPU 计算资源,一块 H100 的市价是 $2-3/小时
- API Key 和凭证:LiteLLM 等代理服务存储了大量下游 LLM 提供商的 API Key
- Agent 的工具调用权限:如果 Agent 有数据库读写、文件操作、API 调用等工具,攻击者可以通过操纵 Agent 间接执行这些操作
- 内部知识库:RAG 系统中的私有文档、代码库索引、业务数据
3. 缺失的纵深防御
成熟的 Web 应用通常有多层安全机制:WAF → 反向代理 → 应用认证 → 数据库权限。但 AI Agent 基础设施在 2026 年仍然处于「能跑就行」阶段:
- 没有 WAF(AI 调用都是 API,传统 WAF 规则不适用)
- 没有反向代理(前文提到的直接暴露习惯)
- 认证只有一层(而且可能就是那个读
request.url.path的中间件) - 没有最小权限(Agent 的 tool 权限通常是 all-or-nothing)
BadHost 不是单独致命的。它致命是因为它攻破的是 AI Agent 安全链上唯一的一个环节。
MCP Server 的特殊风险
在所有受影响的下游项目中,MCP Server 值得单独分析。
可预测的攻击入口
MCP(Model Context Protocol)规范要求每个 MCP Server 暴露一个未认证的 OAuth discovery endpoint:
GET /.well-known/oauth-authorization-server
这个 endpoint 按设计就是公开的——客户端需要先发现 OAuth 配置,然后才能完成认证流程。
对攻击者来说,这是一个完美的入口点。它有三个优势:
- 路径可预测:不需要猜测,直接从规范中得到
- 未认证可访问:认证中间件必然放行这个路径
- 可用于验证注入效果:攻击者先用正常 Host 访问这个 endpoint 确认可达,再用注入 Host 访问受保护 endpoint,如果两次都返回 200,说明注入成功
没有二次认证
MCP Server 的设计角色是 AI Agent 与外部系统之间的认证桥梁。Agent 通过 MCP Server 获取访问 GitHub、Slack、数据库等外部服务的凭证。
一旦攻击者绕过了 MCP Server 的认证中间件,他拿到的不只是 MCP Server 本身的访问权——他拿到的是 Agent 连接的所有外部服务的凭证。
更糟糕的是,MCP Server 到外部服务之间通常没有额外的认证层。MCP Server 持有的 OAuth token 本身就是最终凭证。没有 MFA,没有 IP 白名单,没有二次确认。
攻击链示例
攻击者 → 发送请求到 MCP Server
Host: evil.com/.well-known/oauth-authorization-server?x=#
GET /internal/credentials/github
→ 认证中间件读 request.url.path
→ 得到 "/.well-known/oauth-authorization-server"
→ 判定为公开路径,放行
→ ASGI 路由器读 scope["path"]
→ 得到 "/internal/credentials/github"
→ 返回 Agent 的 GitHub OAuth token
→ 攻击者拿到 token
→ 以 Agent 身份操作 GitHub 仓库
一个 HTTP 请求,从 MCP Server 拿到 GitHub 凭证。整个链条中没有任何环节会触发告警。
修复方案三步走
第一步:升级 Starlette(立即执行)
# 检查当前版本
pip show starlette
# 升级到 1.0.1+
pip install "starlette>=1.0.1"
# 如果用 FastAPI,升级到包含修复的版本
pip install --upgrade fastapi
Starlette 1.0.1 的补丁对 Host header 做了消毒处理,移除了路径分隔符和查询字符串字符。这是最直接的修复。
第二步:代码层修复(本周内完成)
用 X41 发布的工具扫描代码库:
# Semgrep 扫描(推荐)
semgrep --config "p/badhost-cve-2026-48710" .
# 或者用 CodeQL(适合 CI 集成)
# 在 .github/workflows/codeql.yml 中添加 X41 的查询包
所有认证逻辑中 request.url.path 的读取都需要改为 scope["path"]:
# ❌ 修改前
path = request.url.path
# ✅ 修改后
path = scope["path"]
# 或者在 middleware 之外:
path = request.scope["path"]
也可以在 badhost.org 上对已部署的服务做快速扫描。
第三步:部署层加固(下个迭代完成)
在反向代理层固定 Host header:
# Nginx 配置
server {
listen 443 ssl;
server_name api.yourdomain.com;
location / {
proxy_pass http://127.0.0.1:8000;
# 关键:用 server_name 覆盖客户端 Host
proxy_set_header Host $server_name;
# 而不是 proxy_set_header Host $http_host;
}
}
如果使用 Cloudflare 等 CDN,它们默认会用自己的域名覆盖 Host header,已经具备防护效果。但仍然建议显式配置,不要依赖默认行为。
三层防御缺一不可。升级 Starlette 修复了框架本身的 bug,代码改读 scope["path"] 消除了应用层的依赖,反向代理过滤了网络层的恶意输入。只做其中一层不叫修复,叫赌运气。
三类最高风险场景
如果你没有时间做全面排查,至少先检查这三类场景:
场景 1:直接暴露 FastAPI/Starlette 且无反向代理
# 如果你的服务是这样启动的,你在裸奔
uvicorn main:app --host 0.0.0.0 --port 8000
场景 2:直接暴露 LiteLLM / vLLM 端点
这两个项目的默认部署方式就是直接暴露 HTTP 端口。LiteLLM 作为 API 代理还持有大量下游 API Key,风险叠加。
场景 3:认证代码读 request.url.path 而非 scope path
即使有反向代理,如果代理没有正确覆盖 Host header(比如用了 $http_host 而非 $server_name),恶意 Host 仍然会穿透到 Starlette。这种情况下只有代码层修复是可靠的。
架构反思:Python AI 基础设施的信任链问题
BadHost 暴露的不只是一个技术 bug,而是 Python AI 基础设施的信任链设计缺陷。
问题 1:框架假设 ≠ 部署现实
Starlette 的设计假设是「应用运行在受信任的反向代理后面,Host header 是可信的」。这个假设在传统 Web 开发中基本成立——没人会把 Django/Rails 直接暴露在公网上。
但 AI 基础设施打破了这个假设。vLLM 的文档里教你 python -m vllm.entrypoints.openai.api_server --host 0.0.0.0,LiteLLM 的 Quick Start 是 litellm --model gpt-4 --port 4000。这些项目的目标用户是 ML 工程师而非 Web 运维,「在反向代理后面部署」不在他们的思维模型中。
框架的安全假设和实际部署方式之间存在巨大鸿沟。 BadHost 只是这个鸿沟暴露出的第一个高危漏洞,不会是最后一个。
问题 2:Python 生态的隐式依赖链
Starlette 每周 3.25 亿次下载。FastAPI 依赖它,vLLM 依赖 FastAPI,LiteLLM 依赖 FastAPI,MCP Server SDK 依赖 Starlette。这是一条深度为 3-4 层的隐式依赖链。
大多数使用这些项目的团队甚至不知道自己在用 Starlette。他们知道自己在用 vLLM,但不知道 vLLM 的认证中间件依赖 Starlette 的 request.url.path,而 request.url.path 依赖一个未经消毒的 Host header。
这不是 Python 独有的问题(Log4Shell 是 Java 版本),但 Python AI 生态的特殊之处在于:依赖链的末端是大规模 GPU 集群和 AI Agent 的工具调用权限。一个底层库的 bug 放大到了基础设施级别的影响。
问题 3:「能跑就行」文化的代价
AI 领域的速度崇拜造就了一种「先让它跑起来,安全以后再说」的文化。两年前 LangChain 被批评为「安全债务工厂」,今天同样的问题在更底层的基础设施中重演。
一些行业数据可以佐证:
- vLLM 在被审计之前从未进行过独立安全审计(它的第一次审计就是 OSTIF 赞助的这次,发现了 BadHost)
- LiteLLM 的认证文档在 2026 年 5 月之前没有提到 Host header 的安全风险
- 绝大多数 MCP Server 实现没有对 Host header 做任何验证
AI 基础设施需要和金融基础设施一样的安全纪律。 一个能操作数据库、调用外部 API、管理 GPU 资源的系统,不应该运行在连 Host header 都不验证的框架上。
问题 4:CVSS 体系的局限
CVSS 7.0——这是 BadHost 的官方评分。但 X41 和 OSTIF 都公开表示这个分数低估了实际影响。
CVSS 的评分框架是为单一组件设计的:「攻击者能对这个软件做什么」。它没有考虑供应链放大效应:Starlette 的一个 7.0 漏洞,通过 FastAPI → vLLM → AI Agent 这条链路放大后,实际影响可能是「攻击者获得了对整个 AI 基础设施的未授权访问」——这在 CVSS 体系中应该是 9.0+。
安全社区需要一种新的评分方法来处理这种「基础库漏洞 × 大规模下游依赖」的场景。在那之前,不要仅凭 CVSS 分数来决定漏洞的优先级。
行动清单
| 优先级 | 行动 | 时限 |
|---|---|---|
| P0 | 升级 Starlette 到 1.0.1+ | 今天 |
| P0 | 用 Semgrep 扫描代码中的 request.url.path 认证逻辑 | 今天 |
| P1 | 改读 scope path,部署修复版本 | 本周 |
| P1 | 在 badhost.org 扫描已部署服务 | 本周 |
| P2 | 部署反向代理,固定 Host header | 下个迭代 |
| P2 | 审计 MCP Server 的认证配置 | 下个迭代 |
| P3 | 将 Semgrep / CodeQL 规则加入 CI 流水线 | 本月 |
BadHost 提醒我们一件事:AI 基础设施的安全水位,取决于它依赖的最弱组件。 而在 2026 年的 Python AI 生态中,那个最弱组件可能就是你根本不知道自己在用的那个库。