☰
CVE-2026-48710 BadHost 复现与防护:Starlette 主机头注入下,TaoToken 统一 Key 通道的 settings.json 加固骨架
2026/9/27 19:03:06 网站建设 项目流程

1. 当 Host 头变成路径:BadHost 到底在攻击什么

CVE-2026-48710,代号 BadHost,是 Starlette 在重建request.url时对 Host 头缺乏字符校验导致的主机头注入漏洞。它能让一个远程请求在中间件眼里是/evil,在路由匹配时却命中/admin,从而绕过几乎所有基于request.url.path的权限判断。受影响的是 Starlette ≤ 1.0.0,修复版本为 1.0.1。FastAPI、vLLM、LiteLLM、MCP 服务器、Ray Serve、BentoML 这些 Python AI 生态里的常客,只要间接依赖了旧版 Starlette,就都在这条攻击链的射程内。

这个漏洞适合谁关注?如果你正在用 FastAPI 写 AI 网关、用 LiteLLM 做多模型代理、用 vLLM 暴露推理接口,或者用 FastMCP 搭 Agent 工具服务,那它就不是"别人的漏洞"。它的触发成本极低:一个?字符就能让鉴权中间件失效。更麻烦的是,很多开发者根本不知道request.url是基于用户可控的 Host 头重建出来的,误以为它和scope["path"]一样可信。

我试过在本地起一个最小 FastAPI 服务复现,从正常请求被 403 拦截,到加上Host: 127.0.0.1:8000/?后直接返回敏感数据,整个过程不到五分钟。这篇文章会带你走完复现、加固、验证三步,并给出一个可复制的settings.json骨架,把 TaoToken 统一 Key 通道接进来,让请求校验和模型调用都收敛到同一条可控链路上。

2. 复现环境与 TaoToken 前置准备

2.1 最小复现服务

先搭一个有漏洞的服务,用request.url.path做权限判断,这是最典型的错误写法。

# vulnerable_app.py from fastapi import FastAPI, Request, HTTPException from starlette.middleware.base import BaseHTTPMiddleware app = FastAPI(title="BadHost Vulnerable Demo") class PathBasedAuthMiddleware(BaseHTTPMiddleware): async def dispatch(self, request: Request, call_next): protected_paths = ["/admin", "/internal"] for path in protected_paths: if request.url.path.startswith(path): if request.client.host != "127.0.0.1": raise HTTPException(status_code=403, detail="Forbidden") return await call_next(request) app.add_middleware(PathBasedAuthMiddleware) @app.get("/") async def public_endpoint(): return {"message": "public"} @app.get("/admin") async def admin_endpoint(): return {"message": "Sensitive admin data", "api_key": "sk-xxxx"} @app.get("/internal/config") async def internal_config(): return {"config": "internal service configurations"}

安装并启动:

pip install fastapi==0.115.0 starlette==1.0.0 uvicorn uvicorn vulnerable_app:app --host 0.0.0.0 --port 8000

2.2 TaoToken 统一 Key 通道准备

复现完之后,我们需要一条可控的模型调用链路来验证加固效果。TaoToken 提供统一的 Key 与 API 通道,官网入口是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 基址为 https://taotoken.net/api 。你需要先在控制台创建一个 API Key,控制台地址是 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite ,Key 管理页在 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 。接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,模型对话调试页在 https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=chat&utm_campaign=rewrite 。

拿到 Key 之后,我们把它写进一个settings.json,作为服务端统一读取的配置骨架。这个文件同时承载三件事:Host 白名单、TaoToken 通道参数、以及请求校验开关。

3. 可复制的 settings.json 与反向代理加固骨架

3.1 settings.json 加固骨架

{ "server": { "allowed_hosts": ["yourdomain.com", "localhost", "127.0.0.1"], "reject_host_special_chars": true, "trusted_proxy_ips": ["127.0.0.1"] }, "taotoken": { "base_url": "https://taotoken.net/api", "api_key_env": "TAOTOKEN_API_KEY", "default_model": "claude-sonnet-4-20250514", "timeout_seconds": 60, "max_retries": 2 }, "security": { "use_scope_path_for_auth": true, "enable_host_middleware": true, "log_invalid_host": true } }

几个关键点:allowed_hosts是白名单,不在列表里的 Host 直接拒绝;reject_host_special_chars打开后,任何包含/ ? # \空格或控制字符的 Host 头都会被 400 掉;use_scope_path_for_auth强制权限判断走request.scope["path"],而不是request.url.path。taotoken.api_key_env指向环境变量名,避免把 Key 硬编码进文件。

3.2 安全中间件实现

# secure_middleware.py import json import os from fastapi import Request, JSONResponse from starlette.middleware.base import BaseHTTPMiddleware with open("settings.json", "r", encoding="utf-8") as f: CONFIG = json.load(f) SPECIAL_CHARS = set("/?#\\ \t\n\r") class SecureHostMiddleware(BaseHTTPMiddleware): async def dispatch(self, request: Request, call_next): host = request.headers.get("host", "") if CONFIG["server"]["reject_host_special_chars"]: if any(c in host for c in SPECIAL_CHARS): if CONFIG["security"]["log_invalid_host"]: print(f"[BadHost] rejected host={host!r}") return JSONResponse( status_code=400, content={"error": "Invalid Host header"} ) hostname = host.split(":")[0] if hostname not in CONFIG["server"]["allowed_hosts"]: return JSONResponse( status_code=400, content={"error": "Host not allowed"} ) return await call_next(request)

权限判断部分改成:

# 错误:易受 BadHost 影响 if request.url.path.startswith("/admin"): # 正确:使用 ASGI scope 原始路径 if request.scope["path"].startswith("/admin"):

3.3 Nginx 反向代理加固

server { listen 80; server_name yourdomain.com; if ($host ~* "[/?#\\ \t\n\r]") { return 400; } location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }

Nginx 在入口就把非法 Host 拦掉,ASGI 服务只接受来自127.0.0.1的流量,形成双层防护。

4. 验证请求:从恶意 Host 到 TaoToken 通道校验

4.1 复现恶意 Host 注入

先确认漏洞存在。正常请求:

curl -i http://127.0.0.1:8000/admin

返回 403。加上恶意 Host:

curl -i http://127.0.0.1:8000/admin -H "Host: 127.0.0.1:8000/?"

在未修复的 Starlette 1.0.0 上,这会返回 200 和敏感数据。原因是request.url.path被重建为/,中间件认为它不在保护列表里,而路由匹配用的是scope["path"],仍然是/admin。

4.2 加固后验证

升级 Starlette 并挂上SecureHostMiddleware后,同样的恶意请求:

curl -i http://127.0.0.1:8000/admin -H "Host: 127.0.0.1:8000/?"

返回 400Invalid Host header。正常请求:

curl -i http://127.0.0.1:8000/admin -H "Host: 127.0.0.1:8000"

返回 403,权限判断依然生效。

4.3 TaoToken 通道校验

加固完成后,用 TaoToken 统一 Key 通道做一次端到端验证,确认模型调用链路正常。先导出 Key:

export TAOTOKEN_API_KEY="你的Key"

然后用 Python 发一个最小请求:

import os import json import urllib.request with open("settings.json", "r", encoding="utf-8") as f: cfg = json.load(f) api_key = os.environ[cfg["taotoken"]["api_key_env"]] url = f"{cfg['taotoken']['base_url']}/v1/chat/completions" payload = { "model": cfg["taotoken"]["default_model"], "messages": [{"role": "user", "content": "ping"}], "max_tokens": 16 } req = urllib.request.Request( url, data=json.dumps(payload).encode(), headers={ "Authorization": f"Bearer {api_key}", "Content-Type": "application/json" }, method="POST" ) with urllib.request.urlopen(req, timeout=cfg["taotoken"]["timeout_seconds"]) as resp: print(resp.status) print(resp.read().decode()[:200])

返回 200 和模型响应,说明 TaoToken 通道可用。你也可以在模型对话页 https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=chat&utm_campaign=rewrite 手动发一条消息做交叉验证。

5. 本篇常见错排查

升级后仍然被绕过:检查是否有多层中间件顺序问题。SecureHostMiddleware必须加在权限中间件之前,否则权限判断先执行,恶意 Host 已经生效。用app.add_middleware时注意后添加的先执行。

Nginx 返回 400 但服务端没日志:说明请求在代理层就被拦了,这是预期行为。如果想让服务端也记录,把 Nginx 的return 400改成proxy_pass并在应用层拦截,但这样会牺牲一层防护。

request.scope["path"]和request.url.path在正常请求下不一致:正常请求下两者应该一致。如果不一致,说明有代理做了路径重写,检查proxy_set_header和X-Forwarded-Prefix配置。

TaoToken 请求返回 401:确认TAOTOKEN_API_KEY环境变量已导出,且 Key 没有多余空格。可以在 API Keys 页 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 重新生成一个测试 Key。

pip 升级 Starlette 后 FastAPI 报版本冲突:FastAPI 对 Starlette 有版本上限约束。先升级 FastAPI 到最新版,再升级 Starlette。如果仍有冲突,用pip install "starlette>=1.0.1" --upgrade --force-reinstall强制覆盖,然后跑一遍测试。

LiteLLM 代理场景:LiteLLM 1.74.2 到 1.83.6 受 BadHost 与 CVE-2026-42271 利用链影响,升级到 1.83.7 以上。升级后检查/mcp-rest/test/connection端点是否已加认证。

6. 把校验链路收敛到 TaoToken 统一通道

BadHost 的核心教训不是"升级一个库",而是"不要信任任何用户可控输入重建出来的对象"。request.url是重建的,request.headers["host"]是用户可控的,只有scope["path"]和scope["server"]才是 ASGI 服务器解析后的原始值。把权限判断、日志记录、限流策略全部切到scope上,是这次加固的底线。

对于长期跑编码任务和 Agent 服务的团队,建议把模型调用也收敛到统一通道。TaoToken 的 Coding Plan 入口在 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite ,适合需要稳定长连接的场景。Claude Code 相关接入参考 https://taotoken.net/claudecode-anthropic?utm_source=taotoken_aicg_blog_end&utm_content=claudecode-anthropic&utm_campaign=rewrite 。接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,里面有完整的请求示例和错误码说明。

最后留一个实操建议:把settings.json里的allowed_hosts和taotoken.base_url做成环境变量覆盖,这样本地调试、预发、生产可以用同一份代码,只换配置。我踩过的坑是把allowed_hosts写死成localhost,结果容器里跑的时候 Host 是服务名,直接被自己的中间件拦了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询