跳转到帖子

一次 Nginx 反向代理场景下 Host 头注入的复现与修复

精选回复

发布于

背景

最近在做一个内部 Web 系统的代码审计和联调测试时,发现一个比较典型但容易被忽略的问题:业务服务本身没有直接暴露公网,前面挂了 Nginx 做反向代理,但后端应用在生成重置密码链接、回调地址、跳转地址时,直接信任了请求中的 Host 或 X-Forwarded-Host。

这类问题通常不会直接导致 RCE,但在账号体系、邮件链接、OAuth 回调、支付回调等场景里,影响并不低。攻击者可以构造恶意 Host,让系统生成指向攻击者域名的链接,配合钓鱼、令牌泄露、缓存污染等方式进一步利用。

下面以一个简化环境复现 Host 头注入问题,并给出相对稳妥的修复方式。

技术分析

常见的错误写法是应用层通过请求头拼接绝对 URL:

String baseUrl = request.getScheme() + "://" + request.getHeader("Host");
String resetLink = baseUrl + "/reset?token=" + token;

如果前端代理没有规范化 Host,或者后端框架在开启代理信任后读取了 X-Forwarded-Host,攻击者就可以控制生成出来的链接。

典型请求如下:

POST /forgot-password HTTP/1.1
Host: attacker.example
Content-Type: application/x-www-form-urlencoded

[email protected]

如果后端直接使用 Host 生成邮件链接,受害者收到的重置密码链接可能变成:

 Host,但保留或转发了 X-Forwarded-Host:

GET / HTTP/1.1
Host: real.example.com
X-Forwarded-Host: attacker.example

部分框架或中间件在配置了 trust proxy 后,会优先使用 X-Forwarded-Host 生成外部地址。如果没有白名单校验,也会出现同样的问题。

复现环境

这里使用 Flask 写一个最小示例,模拟业务系统生成重置密码链接。

from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route('/forgot-password', methods=['POST'])
def forgot_password():
    email = request.form.get('email')
    token = 'demo-token-123456'

    # 漏洞点:直接信任 Host 头
    base_url = request.scheme + '://' + request.headers.get('Host')
    reset_link = base_url + '/reset?token=' + token

    return jsonify({
        'email': email,
        'reset_link': reset_link
    })

if __name__ == '__main__':
    app.run(host='127.0.0.1', port=5000)

启动服务后,正常请求:

curl -i -X POST ' \
  -H 'Host: app.example.com' \
  -d '[email protected]'

返回内容:

{
  "email": "[email protected]",
  "reset_link": "
}

构造恶意 Host:

curl -i -X POST ' \
  -H 'Host: evil.example.net' \
  -d '[email protected]'

可以看到链接被污染:

{
  "email": "[email protected]",
  "reset_link": "
}

如果这个链接真实发送到邮箱,用户点击后就会把重置 token 带到攻击者控制的域名上。

Nginx 代理场景中的问题点

很多线上系统存在类似配置:

server {
    listen 80;
    server_name app.example.com;

    location / {
        proxy_pass 
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

这里的 $host 会经过 Nginx 规范化处理,但如果 server_name 匹配不严格,或者存在默认站点兜底,仍可能把异常 Host 转发到后端。

更危险的是类似配置:

proxy_set_header Host $http_host;

$http_host 基本取自原始请求头,包含端口,也更容易携带异常值。除非有明确需求,否则不建议直接把 $http_host 透传给后端。

操作步骤:如何排查

实际测试时,可以按下面几个方向检查。

1. 测试 Host 是否可控

curl -i ' \
  -H 'Host: evil.example.net'

观察返回内容、跳转地址、页面中的绝对 URL、接口返回字段是否出现 evil.example.net。

2. 测试 X-Forwarded-Host

curl -i ' \
  -H 'X-Forwarded-Host: evil.example.net' \
  -H 'X-Forwarded-Proto: https'

如果响应里的链接、Location 头、邮件预览、OAuth redirect_uri 出现该域名,需要重点关注。

3. 测试密码重置、邮箱验证等功能

Host 头注入在普通页面可能看不出危害,但在这些功能里影响更明显:

  • 找回密码链接
  • 邮箱验证链接
  • 邀请注册链接
  • SSO/OAuth 回调地址
  • 支付成功/失败跳转地址
  • 后台导出文件下载地址

4. 关注缓存污染

如果前面还有 CDN 或反向代理缓存,可以测试响应是否被缓存。一个错误的 Host 生成结果如果进入缓存,可能导致其他用户看到被污染的链接。

修复方案

1. 应用层不要直接信任请求头生成外部地址

更推荐使用配置项维护站点外部地址:

PUBLIC_BASE_URL= os
from flask import Flask, request, jsonify

app = Flask(__name__)

PUBLIC_BASE_URL = os.environ.get('PUBLIC_BASE_URL', '

@app.route('/forgot-password', methods=['POST'])
def forgot_password():
    email = request.form.get('email')
    token = 'demo-token-123456'

    reset_link = PUBLIC_BASE_URL.rstrip('/') + '/reset?token=' + token

    return jsonify({
        'email': email,
        'reset_link': reset_link
    })

这样生成链接不再依赖用户输入的 Host。

2. Nginx 层限制合法 Host

建议给默认站点直接丢弃非法请求:

server {
    listen 80 default_server;
    server_name _;
    return 444;
}

业务站点只接受明确域名:

server {
    listen 80;
    server_name app.example.com;

    if ($host != 'app.example.com') {
        return 444;
    }

    location / {
        proxy_pass 
        proxy_set_header Host app.example.com;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

这里直接把传给后端的 Host 固定为可信域名,避免后端误用。

3. 清理不必要的转发头

如果后端不需要 X-Forwarded-Host,建议不要转发,或者覆盖为空:

proxy_set_header X-Forwarded-Host "";

如果确实需要,也应该只由可信代理设置,不能让客户端传入的值原样进入后端。

4. 框架层配置可信代理

例如在 Express、Django、Spring Boot 等框架里,很多项目会开启 trust proxy 或 forward headers 支持。开启前要确认只有可信代理可以访问后端服务,否则客户端可以直接伪造转发头。

后端服务应只监听内网地址,例如:

app.run(host='127.0.0.1', port=5000)

容器环境中则建议通过安全组、NetworkPolicy 或防火墙限制访问来源。

注意事项

  • 不要只在前端页面做域名限制,邮件链接、接口返回、跳转响应都要检查。
  • 不要把 Host 校验写成简单 contains,例如 example.com.evil.net 也可能绕过。
  • 多域名业务要使用白名单数组精确匹配,而不是正则随意放宽。
  • HTTPS 场景下还要注意 X-Forwarded-Proto,否则可能生成错误协议的链接。
  • 修复后建议清理缓存,尤其是 CDN、反向代理缓存和应用内部缓存。

总结

Host 头注入本质上是“把客户端可控输入当成可信站点地址”使用。这个问题不一定有醒目的报错,也不一定能在普通页面直接体现,但在账号安全链路里很容易变成实际风险。

比较稳妥的处理方式是:应用层使用固定配置生成外部 URL,代理层限制合法 Host,并清理不必要的转发头。测试时重点覆盖找回密码、邮箱验证、邀请注册、OAuth 回调和支付跳转等功能,通常能发现不少隐藏问题。

网络流量与边界分析示意图
网络请求、日志与边界流量分析示意
这个场景里除了在应用层校验 Host,Nginx 侧建议也做一层“兜底”,否则后端一旦有绝对 URL 拼接、重定向、密码重置链接生成,还是容易被带偏。 可以重点看两个点: 1. 不要把用户传入的 Host 原样透传给后端 如果现在配置里是这样:
proxy_set_header Host $http_host;
# 或
proxy_set_header Host $host;
建议改成明确的后端期望域名,或者至少用白名单 server_name 约束。 例如:
server {
    listen 80 default_server;
    server_name _;
    return 444;
}

server {
    listen 80;
    server_name example.com 

    location / {
        proxy_pass 
        proxy_set_header Host example.com;
        proxy_set_header X-Forwarded-Host example.com;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}
这样即使请求里带:
Host: evil.com
也不会继续传到后端参与业务逻辑。 2. default_server 不要落到正常业务 很多 Host 头注入是因为 Nginx 没匹配到 server_name 时,默认进了第一个 server 块。建议显式加一个 default_server 拦截未知 Host:
server {
    listen 80 default_server;
    listen 443 ssl default_server;
    server_name _;

    return 444;
}
如果是 HTTPS,还要注意 SNI 和 Host 不一致的情况。可以用下面方式测一下:
curl -k -H 'Host: evil.com' 
curl -k --resolve evil.com:443:你的IP 
curl -k --http1.1 -H 'Host: evil.com'  -I
观察响应里的这些位置:
Location:
Set-Cookie:
Refresh:
页面中的绝对链接
密码重置 / 邮件验证链接
OAuth redirect_uri 相关跳转
应用层也最好加白名单,不要只依赖 Nginx。比如后端如果是 Spring Boot,可以配置允许的 host;如果是 Django,需要确认:
ALLOWED_HOSTS = ["example.com", "

修复后可以再用原来的 PoC 复测,重点确认两件事:未知 Host 是否直接被拒绝,以及后端日志里拿到的 Host 是否已经固定为可信域名。这样比单纯过滤某些特殊字符更稳。

			

			
		

创建帐户或登录后发表意见

最近浏览 0

  • 没有会员查看此页面。

SiteMap HACKHAT © 2026 All rights reserved.