Starlette BadHost 漏洞:一个畸形 Host 头如何绕过 AI 代理的访问控制

2026-06-01 29 预计阅读时间:1 分钟
来源:infoq.com AI 摘要 原文链接

免责声明:本文为 AI 摘要整理,建议结合原文阅读。摘要可能省略上下文、版本差异或边界条件,不作为官方说明。

预计阅读时间:10 分钟

Starlette 是 Python 生态中用量最大的 Web 框架之一——每周下载量 3.25 亿,FastAPI、Uvicorn 等基础设施都直接依赖它。最近披露的 BadHost 漏洞(高危)揭示了一个容易被忽视的问题:当路径级访问控制遇上畸形的 HTTP Host 头,防线会从根部断裂。对于暴露在公网的 AI Agent 服务、LLM Gateway 和评测系统来说,这不是理论风险——这些服务普遍用 Starlette/FastAPI 搭建,且访问控制往往只做到路径级别。

漏洞根因:Host 头与路径路由的信任错位

Starlette 的路由匹配依赖请求路径(scope["path"]),而访问控制中间件通常也只检查路径前缀,比如 /admin/* 需要认证、/agent/internal/* 禁止外部访问。问题在于,HTTP 请求的最终路径并不是只由客户端发送的 URI 决定——它还受 Host 头影响。

Host 头被故意构造为畸形值时(例如嵌入路径片段、非标准字符),部分代理、网关和 Starlette 自身的路径解析逻辑会产生不一致的结果。攻击者发送的请求在中间件看来访问的是合法路径,但在路由层实际到达了受保护资源。这就是 BadHost 的核心机制:利用 Host 头畸形值制造路径认知分歧,绕过只看路径的访问控制

受影响的 AI 基础设施场景

AI Agent 服务、LLM Gateway 和评测平台有几个共同特征,使它们尤其脆弱:

  • 多路由共存:同一个服务同时暴露公开 API(/chat/completions)和内部管理接口(/agent/config/eval/results)。
  • 路径级鉴权:用中间件按路径前缀决定是否要求认证,而非对每个路由单独鉴权。
  • 公网可达:Agent 服务需要接收外部调用,管理接口本应只对内部开放,但部署在同一进程。

一个典型的 FastAPI 应用结构如下,其中 internal_router 只做了路径前缀隔离,没有独立鉴权:

# vulnerable_app.py — 展示 BadHost 风险的典型结构
from fastapi import FastAPI
from fastapi.middleware import Middleware

app = FastAPI()

# 公开接口
@app.post("/chat/completions")
async def chat_completion(prompt: str):
    return {"response": "..."}

# 内部管理接口 — 仅靠路径前缀隔离
@app.get("/agent/config")
async def get_agent_config():
    return {"model": "gpt-4", "tools": ["search", "code"]}

@app.get("/eval/results")
async def get_eval_results():
    return {"scores": [0.92, 0.87, 0.95]}

# 路径级访问控制中间件(只检查 path)
@app.middleware("http")
async def path_based_auth(request, call_next):
    path = request.url.path
    if path.startswith("/agent") or path.startswith("/eval"):
        # 这里只看了 path,没验证 Host 的合法性
        # BadHost 可以让这个 path 检查看到的是 /chat/completions
        # 而实际路由到达的是 /agent/config
        if not request.headers.get("Authorization"):
            from fastapi.responses import JSONResponse
            return JSONResponse(status_code=401, content={"error": "unauthorized"})
    return await call_next(request)

上面的中间件只检查 request.url.path,没有对 Host 头做任何校验。在 BadHost 场景下,攻击者可以构造畸形 Host 头,让中间件看到的路径和路由层实际匹配的路径不一致。

攻击演示:用畸形 Host 头绕过路径鉴权

以下脚本模拟 BadHost 攻击的核心手法——发送带有畸形 Host 头的请求,尝试绕过路径级访问控制访问内部接口:

# badhost_probe.sh — 检测你的服务是否受 BadHost 影响
# 用法: bash badhost_probe.sh <target_host> <target_port>
# 注意: 仅用于对自己服务的安全测试,勿用于未授权系统

TARGET_HOST="${1:-localhost}"
TARGET_PORT="${2:-8000}"

echo "=== BadHost 漏洞探测 ==="
echo "目标: ${TARGET_HOST}:${TARGET_PORT}"

# 1. 正常请求 — 应返回 401
echo ""
echo "[1] 正常请求 /agent/config(无 Auth 头):"
curl -s -o /dev/null -w "HTTP %{http_code}" \
  "http://${TARGET_HOST}:${TARGET_PORT}/agent/config"

# 2. 畸形 Host 头 — 带路径片段
echo ""
echo "[2] 畸形 Host: 嵌入路径片段:"
curl -s -o /dev/null -w "HTTP %{http_code}" \
  -H "Host: ${TARGET_HOST}/chat/completions" \
  "http://${TARGET_HOST}:${TARGET_PORT}/agent/config"

# 3. 畸形 Host 头 — 非标准字符
echo ""
echo "[3] 畸形 Host: 非标准字符注入:"
curl -s -o /dev/null -w "HTTP %{http_code}" \
  -H "Host: ${TARGET_HOST}%2fchat%2fcompletions" \
  "http://${TARGET_HOST}:${TARGET_PORT}/agent/config"

# 4. Host 头包含端口欺骗
echo ""
echo "[4] 畸形 Host: 端口欺骗:"
curl -s -o /dev/null -w "HTTP %{http_code}" \
  -H "Host: ${TARGET_HOST}:${TARGET_PORT};path=/chat/completions" \
  "http://${TARGET_HOST}:${TARGET_PORT}/agent/config"

echo ""
echo ""
echo "如果任何畸形 Host 请求返回 200 而非 401,你的服务受 BadHost 影响。"

运行方式:

# 启动测试服务
pip install fastapi uvicorn
python vulnerable_app.py &  # 或 uvicorn vulnerable_app:app &

# 探测
bash badhost_probe.sh localhost 8000

修复方案:多层防御

BadHost 的教训是:不要只靠路径做访问控制。修复需要从三个层面入手。

第一层:严格校验 Host 头

在中间件最早的位置加入 Host 头白名单校验,拒绝任何不符合预期的请求:

# fixed_app.py — 修复后的版本
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
import re

ALLOWED_HOSTS = {"api.yourcompany.com", "localhost"}  # 改为你的实际域名

app = FastAPI()

@app.middleware("http")
async def validate_host_then_auth(request: Request, call_next):
    # 第一关: Host 头白名单
    host = request.headers.get("host", "").split(":")[0]  # 去掉端口
    if host not in ALLOWED_HOSTS:
        return JSONResponse(status_code=400, content={"error": "invalid host"})

    # 第二关: 路径级鉴权(现在 Host 已验证,路径认知不会分歧)
    path = request.url.path
    if path.startswith("/agent") or path.startswith("/eval"):
        if not request.headers.get("Authorization"):
            return JSONResponse(status_code=401, content={"error": "unauthorized"})

    return await call_next(request)

# 公开接口
@app.post("/chat/completions")
async def chat_completion(prompt: str):
    return {"response": "..."}

# 内部接口
@app.get("/agent/config")
async def get_agent_config():
    return {"model": "gpt-4", "tools": ["search", "code"]}

第二层:路由级鉴权,而非路径前缀级

不要用中间件按前缀拦截,而是对每个敏感路由单独加依赖项鉴权:

from fastapi import Depends, HTTPException

def require_auth(authorization: str = None):
    if not authorization or not authorization.startswith("Bearer "):
        raise HTTPException(status_code=401)
    # 实际验证 token...
    return authorization

# 每个内部路由单独鉴权,不依赖中间件的前缀匹配
@app.get("/agent/config", dependencies=[Depends(require_auth)])
async def get_agent_config():
    return {"model": "gpt-4", "tools": ["search", "code"]}

第三层:网络层隔离

最可靠的方案是把内部接口从公网服务中彻底拆出来:

# docker-compose.yml — 内部接口走独立端口,不暴露公网
services:
  ai-gateway:
    build: .
    ports:
      - "443:8000"   # 公网只到公开 API
    environment:
      - SERVE_INTERNAL=false
    command: uvicorn app:app --host 0.0.0.0 --port 8000

  internal-api:
    build: .
    ports:
      - "127.0.0.1:8001:8001"  # 只绑定 localhost,外部不可达
    environment:
      - SERVE_PUBLIC=false
    command: uvicorn internal_app:app --host 0.0.0.0 --port 8001

自检清单

部署 AI Agent 服务或 LLM Gateway 前,逐项确认:

检查项 状态
Starlette/FastAPI 版本是否包含 BadHost 补丁
是否有中间件仅靠路径前缀做鉴权
Host 头是否有白名单校验
内部管理接口是否与公开 API 在同一进程
内部接口是否可通过公网端口直接访问
是否对每个敏感路由单独加了鉴权依赖

BadHost 暴露的不是某个罕见边界条件,而是一个系统性设计盲区:当安全决策依赖的输入字段(路径)可以被另一个不受控的输入字段(Host)间接篡改时,防线就是虚的。对于 AI 基础设施来说,内部接口泄露的后果远不止数据外泄——Agent 配置、评测结果、模型参数都可能被篡改或窃取,进而影响下游所有依赖该 Agent 的业务。升级框架版本是第一步,但真正解决问题需要重新审视访问控制的架构:从路径前缀拦截走向路由级鉴权,从单进程混合部署走向网络层隔离。


相关推荐