Skip to content

Host Header 注入与路由越权防御 ​

Host 头注入的本质是边缘代理、CDN 与应用框架对请求主机名的信任边界不一致,以及业务代码将不可信请求头直接拼入绝对 URL、服务间调用地址或缓存键。完整防线必须同时覆盖网关层白名单、覆写头剥离、应用静态基准域名与缓存键隔离。

威胁面与防御映射 ​

攻击向量根因机制防御控制点
密码重置 / 邮箱验证链接劫持后端用 Host 或 X-Forwarded-Host 拼接 reset/verify URL强制从服务端配置读取 CANONICAL_ORIGIN,禁止动态取请求头
Override Header 优先级绕过X-Forwarded-Host、X-Host、X-Original-Host、X-Forwarded-Server、X-HTTP-Host-Override、Forwarded 被框架优先采信边缘网关无条件清除客户端传入的覆写头;仅对可信代理 CIDR 显式开启
Host 语法混淆(端口/@/#/双编码)URL 解析器与字符串白名单对 target.com@evil.com、target.com:1337、127.0.0.1 解析不一致使用严格 ^[a-z0-9.-]+$ + 精确集合匹配,并在首跳 Nginx default_server 直接 444/400 拒绝
Host → SSRF 内网探测服务间调用使用 http://{Host}/internal/health内部调用地址必须走服务注册表或静态环境变量,严禁引入请求 Host
Host 鉴权绕过 / Debug 触发以 Host == admin.internal 或 localhost 跳过认证或开启调试模式鉴权与调试开关绑定内部网络隔离与独立凭据,不得依赖 HTTP Host
Host → Cache PoisoningCDN/Varnish 缓存未将 Host 或覆写头纳入 Cache Key,却在 Location、<link rel="canonical">、OpenGraph 中回显剥离未授权覆写头;规范化并固定跳转与页面内的绝对域名

防御工作流 ​

text
1. 边界入口拦截(Nginx/Envoy/ALB)
   └─ 配置 catch-all default_server 返回 400/444,只放行精确 server_name
2. 覆写头清洗(Trusted Proxy Policy)
   └─ 在边界删除客户端自带的 X-Forwarded-Host / X-Host / X-Original-Host / Forwarded
3. 应用框架加固(Django/Express/Spring/Flask)
   └─ 启用 ALLOWED_HOSTS / trust proxy 白名单,禁用绝对 URL 动态推导
4. 业务 URL 生成隔离
   └─ 密码重置、OAuth redirect_uri、支付 notify_url、Sitemap 统一使用静态 CANONICAL_ORIGIN
5. 缓存与内网调用隔离
   └─ 响应中不反射未校验 Host;内部微服务调用使用固定 Service Discovery 端点

1. Nginx 边界拦截与覆写头清洗 ​

nginx
# 默认兜底虚拟主机:拦截所有未知 Host(含 IP 直连、@/# 混淆、内网主机名)
server {
    listen 80 default_server;
    listen 443 ssl default_server;
    ssl_certificate     /etc/nginx/ssl/fallback.crt;
    ssl_certificate_key /etc/nginx/ssl/fallback.key;
    return 444;
}

# 生产业务虚拟主机
server {
    listen 443 ssl;
    server_name app.example.com;

    location / {
        # 强制使用静态配置的主机名,防止后端框架读取伪造覆写头
        proxy_set_header Host app.example.com;
        proxy_set_header X-Forwarded-Host "";
        proxy_set_header X-Host "";
        proxy_set_header X-Original-Host "";
        proxy_set_header X-Forwarded-Server "";
        proxy_set_header X-HTTP-Host-Override "";
        proxy_set_header Forwarded "";
        proxy_pass http://127.0.0.1:8080;
    }
}

2. Python 应用层 Host 校验与静态绝对 URL 生成 ​

python
import re
from urllib.parse import urlsplit

ALLOWED_HOSTS = frozenset({"app.example.com", "api.example.com"})
CANONICAL_ORIGIN = "https://app.example.com"
HOST_TOKEN_RE = re.compile(r"^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$")


def validate_host_header(raw_host: str, allowed_ports: frozenset[str] = frozenset({"443"})) -> str:
    """严格校验 Host 头,拒绝非授权端口、userinfo(@)、fragment(#) 与未知域名。"""
    if not raw_host:
        raise ValueError("missing host header")
    candidate = raw_host.strip().lower()
    if any(ch in candidate for ch in ("@", "#", "/", "\\", "%", " ")):
        raise ValueError("illegal character in host header")
    if ":" in candidate:
        hostname, port = candidate.split(":", 1)
        if port not in allowed_ports:
            raise ValueError(f"disallowed port in host header: {port}")
    else:
        hostname = candidate
    if not HOST_TOKEN_RE.fullmatch(hostname):
        raise ValueError("malformed hostname")
    if hostname not in ALLOWED_HOSTS:
        raise ValueError(f"untrusted host: {hostname}")
    return hostname


def build_password_reset_url(token: str) -> str:
    """密码重置/邮箱验证链接严禁使用请求 Host,必须绑定静态 CANONICAL_ORIGIN。"""
    origin = urlsplit(CANONICAL_ORIGIN)
    if origin.scheme != "https" or origin.hostname not in ALLOWED_HOSTS:
        raise RuntimeError("invalid canonical origin configuration")
    return f"{CANONICAL_ORIGIN}/reset?token={token}"

验证与判定标准 ​

运行下方自检脚本验证 Host 校验器对 host-header.md 中列举的全部绕过变体均返回拒绝,且密码重置链接始终指向固定可信域:

python
def run_host_hardening_checks() -> None:
    malicious_hosts = [
        "attacker.com",
        "app.example.com:1337",
        "app.example.com@attacker.com",
        "attacker.com#app.example.com",
        "attacker.com%23app.example.com",
        "attacker.com%2fapp.example.com",
        "app.example.com.evil.com",
        "localhost",
        "127.0.0.1",
        "[::1]",
        "169.254.169.254",
    ]
    for bad in malicious_hosts:
        try:
            # 含端口或非法域名均应被严格拦截
            host_only = validate_host_header(bad)
            if ":" in bad:
                raise AssertionError(f"port variant bypassed check: {bad}")
            if host_only not in ALLOWED_HOSTS:
                raise AssertionError(f"untrusted host accepted: {bad}")
        except ValueError:
            pass
        else:
            raise AssertionError(f"malicious host not rejected: {bad}")

    assert validate_host_header("app.example.com") == "app.example.com"
    reset_url = build_password_reset_url("tok-123")
    assert reset_url == "https://app.example.com/reset?token=tok-123"
    print("PASS: all host-header hardening checks passed")


if __name__ == "__main__":
    run_host_hardening_checks()

判定标准:

  1. 边缘拒止率 100%:向业务端口发送任意未知 Host 或携带 X-Forwarded-Host/X-Original-Host/Forwarded 时,响应 Location、正文 URL 与邮件模板均不包含外部主机名。
  2. 端口与解析器一致性:带 @、#、%2f、[::1] 与非授权端口的 Host 头全部返回 400/444。
  3. 无内部端点映射:修改 Host 为 127.0.0.1 或 169.254.169.254 不触发差异状态码、耗时变化或调试栈回显。

MCP 工具映射 ​

防御阶段MCP 工具说明
防御知识定位kb_router按 Host header、X-Forwarded-Host、Cache Poisoning 检索对应防御条目
规程读取kb_read_file读取 host-header-defense 与关联 SSRF/缓存文档
知识目录核对kb_catalog列出 02-auth 与 08-infra 下的配对防御文档

Apache-2.0 · 仅供授权环境下的学习与防御性研究使用 · 免责声明