Host 头攻击防御实战:Nginx 与后端框架校验配置

适用场景

Host 头攻击(Host Header Attack)是 Web 应用中最容易被忽视的一类输入信任缺陷。反向代理只做转发、后端框架默认信任 HTTP_HOST,攻击者篡改请求头中的 Host 字段即可影响业务逻辑。典型受害场景包括:

  • 密码重置链接投毒:系统用 $_SERVER['HTTP_HOST'] 拼接重置链接,攻击者提交他人邮箱并伪造 Host,收到的邮件中链接指向攻击者域名,用户点击后令牌被窃取。
  • 缓存投毒与虚拟主机混淆:CDN 按 Host 分区缓存,脏缓存被投放到正常域名下。
  • 绕过基于 Host 的访问控制:内网管理系统只允许 admin.internal 访问,攻击者直接改 Host 头即可穿透基于域名的限制。
  • 邮件头注入与 SSRF 跳板:Host 值被写入邮件模板或用于构造后端调用地址。

本文给出 Nginx 层、后端框架层、反向代理链路三处的可复制配置,并以 curl 构造多种畸形 Host 完成验证,适用于 Nginx + PHP / Java / Node 的常见部署结构。

前置条件

  • 服务器已部署 Nginx(本文以 1.24+ 为例),具备 /etc/nginx/conf.d/ 写权限
  • 已明确业务实际使用的全部域名清单,包含主域名、www 域名、API 子域、测试域名,这是白名单的基础
  • 后端应用可修改配置(PHP、Django、Spring Boot、Express 任一)
  • 具备一台可访问源站的测试机,且已安装 curl 7.55+,用于构造自定义 Host 头
  • 若源站位于 CDN 之后,需同时掌握回源 Host 的设定方式(多数 CDN 默认保持用户 Host 回源)

原理说明

HTTP/1.1 规定 Host 头由客户端自行填写,服务端无法验证其真实性,只能校验其是否属于允许集合。Nginx 提供两个变量,行为差异是配置正确与否的关键:

  • $http_host原样保留客户端发送的 Host 头,包含多余空格外的原始大小写与端口,例如 Evil.com:443 会完整保留。
  • $host:取自请求行中的主机名或 Host 头,统一转小写并去除端口,若两者均不存在则回退为匹配到的 server_name

由于 $host 在缺少 Host 头时会回退为 server_name,攻击者仍可用无 Host 头的 HTTP/1.0 请求触发回退路径,因此白名单校验必须基于 $http_host 做严格全等匹配,而不是依赖 $host 是否为空。

链路层面还有一类隐藏入口:反向代理与 CDN 会追加 X-Forwarded-HostX-Original-Host 等头。如果后端框架优先读取这些头,那么仅校验 Host 就形同虚设。防御要覆盖”客户端可控的所有主机类请求头”。

操作步骤

1. 配置默认虚拟主机,丢弃未知 Host

第一个匹配的 default_server 会接住所有未命中 server_name 的请求。将它配置为直接断开连接,可以从入口过滤掉绝大多数扫描与畸形 Host 请求:

# /etc/nginx/conf.d/00-default-reject.conf
server {
    listen 80 default_server;
    listen 443 ssl default_server;
    server_name _;

    ssl_certificate     /etc/nginx/ssl/default.crt;
    ssl_certificate_key /etc/nginx/ssl/default.key;

    # 444 为 Nginx 私有指令:直接关闭连接,不回任何响应
    return 444;
}

注意 return 444 不返回响应体,因此 CDN 健康检查若使用 IP 直连并携带真实 Host,仍能正常命中业务 server 块;只有完全未知的 Host 才会被断开。

2. 在业务 server 块做白名单硬校验

map 建立允许集合,再在 server 层拦截。相比在每个 location 里写 if,map 的匹配在请求处理早期完成,开销更低:

# /etc/nginx/conf.d/host-whitelist.conf
map $http_host $host_allowed {
    default                 0;
    "www.example.com"       1;
    "example.com"           1;
    "api.example.com"       1;
    # 显式列出带端口的常见写法,避免漏判
    "www.example.com:443"   1;
    "example.com:443"       1;
    ~^www\.example\.com:\d+$ 1;
}

server {
    listen 443 ssl;
    server_name www.example.com example.com api.example.com;

    # 421 Misdirected Request:语义上准确表达"请求被导向了错误的服务器"
    if ($host_allowed = 0) {
        return 421;
    }
    ...
}

关键参数说明:

  • map 的 default 必须是 0(拒绝),白名单模式不允许兜底放行。
  • 含端口的 Host 必须显式覆盖,否则标准浏览器在某些代理场景下会发送带端口的 Host 而被误杀。
  • 若同时存在 X-Forwarded-Host,可再建一个 map 一并校验:if ($http_x_forwarded_host) { return 421; },前提是链路中没有 CDN 依赖该头。

3. 反代场景清除伪造的主机类请求头

逐跳转发时,客户端自带的 X-Forwarded-* 必须被覆盖而非追加,否则后端会读到伪造值:

location / {
    proxy_pass http://127.0.0.1:8080;

    proxy_set_header Host              $host;
    proxy_set_header X-Real-IP         $remote_addr;
    # 关键:用 $remote_addr 覆盖,而不是 $proxy_add_x_forwarded_for(后者会拼接客户端值)
    proxy_set_header X-Forwarded-For   $remote_addr;
    proxy_set_header X-Forwarded-Host  $host;
    proxy_set_header X-Forwarded-Proto $scheme;
}

如果业务确实位于多级代理之后,X-Forwarded-For 需要使用真实 IP 模块(realip)先还原可信链,再传给后端,避免直接拼接不可信输入。

4. 后端框架层校验

Nginx 只能保护经过它的流量。内网直连、容器间调用、本地调试端口都可能绕过 Nginx,因此后端必须自行校验:

# PHP:所有生成绝对 URL 的地方统一走白名单
$allowed = ['www.example.com', 'example.com', 'api.example.com'];
$host = strtolower(explode(':', $_SERVER['HTTP_HOST'] ?? '')[0]);
if (!in_array($host, $allowed, true)) {
    http_response_code(421);
    exit('Invalid Host');
}
define('SITE_URL', 'https://www.example.com');   // 绝对 URL 用固定配置,禁止用 Host 拼接
# settings.py(Django)
ALLOWED_HOSTS = ["www.example.com", "example.com", "api.example.com"]
# 生产环境禁止使用通配符 "*";CSRF 校验域名同样必须显式列出
CSRF_TRUSTED_ORIGINS = ["https://www.example.com", "https://example.com"]
# application.yml(Spring Boot)
server:
  port: 8080
  tomcat:
    # 严格模式:Tomcat 会校验 Host 头与 serverName 是否一致
    use-relative-redirects: true
  forward-headers-strategy: framework   # 仅在确定有可信代理时开启
# 自定义过滤器示例:校验后写入固定 baseUrl,用于生成邮件链接
app:
  public-base-url: https://www.example.com

核心原则:凡是用于生成对外链接、跳转地址、邮件内容的域名,一律取服务端固定配置,绝不从请求头推导。这是从业务逻辑上根除 Host 头投毒的最有效手段。

配置验证

1. 语法与重载

nginx -t && nginx -s reload

2. 构造畸形 Host 请求

# 正常域名:期望 200
curl -s -o /dev/null -w "%{http_code}\n" -H "Host: www.example.com" https://SERVER_IP/

# 未授权域名:期望 421
curl -s -o /dev/null -w "%{http_code}\n" -H "Host: evil.com" https://SERVER_IP/

# 未授权域名 + 白名单域名作为前缀:期望 421
curl -s -o /dev/null -w "%{http_code}\n" -H "Host: evil.com.www.example.com" https://SERVER_IP/

# 无 Host 的 HTTP/1.0 请求:期望 444 或 421,不得返回业务页面
curl -s -o /dev/null -w "%{http_code}\n" --http1.0 https://SERVER_IP/ -H "Host: "

# 伪造 X-Forwarded-Host:期望 421
curl -s -o /dev/null -w "%{http_code}\n" -H "Host: www.example.com" \
     -H "X-Forwarded-Host: evil.com" https://SERVER_IP/

验证通过的标准是:只有白名单内的域名返回 200,其余全部返回 421 或 444,且响应体中不包含任何业务内容。

3. 业务级验证(最关键一步)

# 触发密码重置流程,抓取邮件中的链接
curl -s -X POST https://www.example.com/api/password/reset \
     -H "Host: evil.com" \
     -d "email=test@example.com"

检查邮件正文中的重置链接。要求链接域名恒为 https://www.example.com/。若出现 evil.com,说明应用层仍在拼接 Host,需回到第 4 步修正。

4. 日志侧确认

# 统计被拒绝的 Host,观察是否有持续的扫描行为
grep -E 'return 421|return 444' /var/log/nginx/error.log | tail -50
awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20

常见问题

FAQ 1:已经部署了云 WAF,源站还需要校验 Host 吗?

需要。WAF 只处理经过它的流量,而源站 IP 一旦泄露,攻击者可直连源站并完全绕过 WAF。通过 CDN 回源白名单(仅允许 CDN 回源段访问 443)可以大幅收敛风险,但回源请求中的 Host 依然由攻击者控制。两者叠加才完整:网络层限制来源 IP,应用层校验 Host

FAQ 2:校验 $host 还是 $http_host?为什么结果不一样?

必须用 $http_host。举例:客户端发送 Host: WWW.EXAMPLE.COM:443$http_host 得到原值,$host 得到 www.example.com。反过来说,$host 的”规范化”看似便利,但当请求完全没有 Host 头时它会回退为 server_name,此时 $host 恒等于合法域名,校验形同虚设。$http_host 为空则明确表示异常请求,可以直接拒绝。

FAQ 3:加了严格白名单后监控探针大量报 503,如何排查?

优先看探针使用的 Host。常见原因有三类:一是探针用 IP 直连但未设置 Host,被 default_server 的 444 断开;二是探针使用了内部服务名(如 svc-mesh.local)作为 Host,该名称未在白名单中;三是负载均衡健康检查使用带端口写法且未在 map 中覆盖。解决方式是在 map 中为内部名称与带端口写法补充条目,或让探针改用业务域名。切勿为了省事把 default 改成 1。

总结

Host 头攻击的根因是”服务端把不可信输入当作了可信配置”。防御按三层展开,缺一不可:

  • 入口层default_server 返回 444,丢弃未声明的域名。
  • 代理层:基于 $http_host 的白名单 map 严格全等匹配,返回 421;同时覆盖 X-Forwarded-* 而非追加。
  • 应用层:框架配置显式域名清单,所有对外链接的域名取服务端固定配置。

验证环节必须包含业务级断言——检查密码重置邮件中的链接域名,只有这一项通过,才能认为 Host 头注入的业务风险被真正消除。