Nginx Host头安全验证漏洞排查与修复实战
最近在做安全测试时,遇到一个关于Host头验证的安全漏洞。这个问题涉及到Nginx代理配置中的Host头处理,以及如何正确地在反向代理层面进行安全验证。记录一下排查和修复过程。
问题现象
安全测试发现,当请求的地址是 http://api.example.com:80/getUser 时,攻击者可以通过修改请求头中的Host字段来绕过后端验证:
api.example.com:1234- 非标准端口api.example.com.cn:80- 错误域名api.example.com:123213- 超出范围端口api.example.com:123aaa- 非法端口格式
这些请求虽然访问的是正确的服务器地址,但Host头被篡改,如果后端服务依赖Host头做安全验证,就可能导致绕过。
初步配置与问题
最初的Nginx配置是这样的:
nginx
location /api/ {
proxy_pass http://backend:26682;
proxy_set_header Host $Host;
# ... 其他配置
}
使用 $Host 变量的问题在于,它只包含主机名,不包含端口。这意味着无论客户端请求时带什么端口,传递给后端的Host头都只有域名部分。
这导致了一个问题:虽然后端做了Host头拦截验证,但收到的始终是 api.example.com(不带端口),而后端可能期望验证完整的Host(包括端口),导致验证逻辑失效。
第一次尝试:使用$http_host
为了解决这个问题,我把配置改成了:
nginx
proxy_set_header Host $http_host;
$http_host 变量会包含客户端发送的完整Host头,包括端口号。
这样修改后,后端确实能收到完整的Host头了,但出现了新的问题:
- 合法但非标准的端口(如
:1234):能进入后端,但后端拦截逻辑可能没有正确处理 - 非法端口格式(如
:123aaa):被Nginx直接拦截返回400错误,根本到不了后端验证逻辑 - 超大端口号(如
:123213):同样可能在Nginx层就被拦截
这说明仅仅改变Host头传递方式是不够的,我们需要在Nginx层面就进行Host头验证。
根本原因分析
问题的根源在于:
-
Nginx的变量差异:
$host: 优先级为 host行 > Host头 > 请求行中的主机名,不包含端口$http_host: 客户端发送的原始Host头,包含端口$server_name: server块中配置的server_name
-
验证层级不对: 在反向代理场景下,应该让Nginx作为第一道防线,而不是把非法请求传递给后端
-
端口验证缺失: 没有在代理层面对端口的合法性进行验证
最终解决方案
方案一:使用if语句进行Host验证
nginx
location /api/ {
# Host头白名单验证
if ($http_host !~ "^api\.example\.com(:80)?$") {
return 403 "Invalid Host header";
}
# 路径遍历防护
if ($request_uri ~* "(\.\.|\/\/|\\\\)") {
return 403;
}
proxy_pass http://backend:26682;
# 传递标准化的Host头给后端
proxy_set_header Host $server_name;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
方案二:使用map指令(推荐)
更优雅的做法是在http块中使用map指令定义合法Host:
nginx
http {
# 定义合法的Host白名单
map $http_host $allowed_host {
default 0;
"api.example.com" 1;
"api.example.com:80" 1;
}
server {
listen 80;
server_name api.example.com;
# 全局Host验证
if ($allowed_host = 0) {
return 403 "Invalid Host header";
}
location /api/ {
# 路径遍历防护
if ($request_uri ~* "(\.\.|\/\/|\\\\)") {
return 403;
}
proxy_pass http://backend:26682;
# 传递标准化的Host,不带端口
proxy_set_header Host $server_name;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $server_name;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket支持
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
}
}
关键要点总结
-
验证层级要前置: 在Nginx反向代理层进行Host验证,不要把非法请求传递给后端
-
选择合适的变量:
- 验证时使用
$http_host获取客户端原始Host头 - 传递给后端时使用
$server_name确保格式统一
- 验证时使用
-
标准化传递给后端: 无论客户端发来什么Host头,传递给后端的都应该是标准化的、经过验证的值
-
添加X-Forwarded头: 如果后端需要知道原始请求信息,通过X-Forwarded-*头传递,而不是直接传递Host头
-
使用白名单模式: 明确定义允许的Host列表,拒绝所有其他值
验证方法
配置完成后,可以用curl命令测试:
bash
# 正常请求 - 应该成功
curl -H "Host: api.example.com" http://api.example.com/api/test
# 正常请求带80端口 - 应该成功
curl -H "Host: api.example.com:80" http://api.example.com/api/test
# 错误域名 - 应该返回403
curl -H "Host: api.example.com.cn" http://api.example.com/api/test
# 非标准端口 - 应该返回403
curl -H "Host: api.example.com:1234" http://api.example.com/api/test
# 非法端口格式 - 应该返回400或403
curl -H "Host: api.example.com:abc" http://api.example.com/api/test
后续建议
-
配置加固: 除了Host头验证,还应配置其他安全头,如
server_tokens off; -
日志监控: 记录被拒绝的Host头请求,便于发现攻击模式
-
定期审计: 随着业务发展,可能需要添加新的合法域名到白名单
-
后端验证: 即使前端做了验证,后端也应该保留验证逻辑作为纵深防御
这次问题排查让我对Nginx的Host头处理有了更深入的理解,也认识到在反向代理场景下安全验证应该在哪一层进行。希望这篇文章能帮助到遇到类似问题的朋友。