Long-form

BadHost 深度分析:一个 Host Header 如何击穿百万 AI Agent 的认证防线

9 min read ·

💡 一句话总结:一个 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 基础设施的部署习惯完全不同:

这种部署习惯意味着恶意 Host header 可以不经任何过滤直接到达 Starlette。

2. 高价值的攻击目标

绕过传统 Web 应用的认证,攻击者拿到的可能是用户数据。但绕过 AI Agent 基础设施的认证,攻击者拿到的是:

3. 缺失的纵深防御

成熟的 Web 应用通常有多层安全机制:WAF → 反向代理 → 应用认证 → 数据库权限。但 AI Agent 基础设施在 2026 年仍然处于「能跑就行」阶段:

BadHost 不是单独致命的。它致命是因为它攻破的是 AI Agent 安全链上唯一的一个环节。

MCP Server 的特殊风险

在所有受影响的下游项目中,MCP Server 值得单独分析。

可预测的攻击入口

MCP(Model Context Protocol)规范要求每个 MCP Server 暴露一个未认证的 OAuth discovery endpoint

GET /.well-known/oauth-authorization-server

这个 endpoint 按设计就是公开的——客户端需要先发现 OAuth 配置,然后才能完成认证流程。

对攻击者来说,这是一个完美的入口点。它有三个优势:

  1. 路径可预测:不需要猜测,直接从规范中得到
  2. 未认证可访问:认证中间件必然放行这个路径
  3. 可用于验证注入效果:攻击者先用正常 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 被批评为「安全债务工厂」,今天同样的问题在更底层的基础设施中重演。

一些行业数据可以佐证:

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 生态中,那个最弱组件可能就是你根本不知道自己在用的那个库。

Frequently asked questions

BadHost 漏洞的根本原因是什么?和 Host header 有什么关系?
Starlette 在构造 request.url 时直接使用客户端传入的 Host header 拼接完整 URL,没有对 Host 值做任何消毒。攻击者在 Host 中注入 /、?、# 等路径分隔符后,生成的 request.url.path 会与 ASGI scope 中的真实 path 不一致。任何读 request.url.path 做认证判断的中间件都会被绕过,因为它看到的是攻击者伪造的路径而非实际请求路径。
我的 FastAPI 服务部署在 Nginx 后面,还需要担心 BadHost 吗?
如果 Nginx 配置了 proxy_set_header Host 为固定值(如 $server_name 而非 $http_host),那么客户端的恶意 Host header 会被覆盖,攻击无法到达 Starlette。但如果 Nginx 直接透传客户端 Host(默认行为),漏洞仍然存在。最安全的做法是三层防御:升级 Starlette 1.0.1+、代码改读 scope path、反向代理固定 Host。只依赖其中任何一层都不够稳。
MCP Server 为什么在这个漏洞中特别危险?
MCP Server 有两个先天劣势。第一,MCP 协议规范要求暴露未认证的 OAuth discovery endpoint(/.well-known/oauth-authorization-server),这给攻击者提供了可预测的入口点来测试 Host 注入是否生效。第二,MCP Server 作为 AI Agent 与外部系统的认证桥梁,一旦被穿透就等于拿到了 Agent 访问所有外部服务的凭证,没有二次认证兜底。攻击者不需要猜路径,直接从 OAuth endpoint 开始打即可。
CVSS 评分只有 7.0,这个漏洞真的严重吗?
CVSS 7.0 是基于单个组件(Starlette)的评估,没有考虑下游放大效应。Starlette 每周 3.25 亿次下载,FastAPI 基于它构建,vLLM、LiteLLM、MCP Server 等 AI 基础设施也依赖它。漏洞发现者 X41 D-Sec 和审计赞助方 OSTIF 都公开表示该评分低估了实际风险。在 AI Agent 场景中,一个认证绕过意味着攻击者可以访问受限 LLM 端点、窃取 API key、操纵内部 Agent 工具、滥用 GPU 计算资源,综合影响远超传统 Web 应用。
除了升级 Starlette,还有哪些具体的检测和修复手段?
X41 D-Sec 发布了三套检测工具:Semgrep 规则可扫描代码中所有读 request.url.path 的认证逻辑、CodeQL 查询可在 CI 流水线中自动拦截、在线扫描器 badhost.org 可快速检查已部署服务。修复方面,除了升级 Starlette 1.0.1+,代码层需要把所有认证中间件中的 request.url.path 改为 scope 中的 path,部署层需要在反向代理中固定 Host header 为服务端已知值,三层叠加才能形成有效纵深防御。
// next.txt ›

Some outbound links in this post are affiliate links — see disclosure.