51人参与 • 2026-08-29 • https
那天下午,我正在处理一个常规的线上服务部署,突然收到一条来自安全团队的告警邮件,标题赫然写着“疑似http host头攻击尝试”。点开详情一看,攻击者试图通过篡改http请求中的host头,来绕过我们某个web应用的访问控制,甚至尝试进行密码重置、缓存投毒等恶意操作。这已经不是第一次了,但这次告警直接关联到了我们使用nginx作为前端代理的几台业务服务器。我意识到,是时候把nginx层针对http host头攻击的防护配置做一次彻底的梳理和加固了。
http host头攻击,对于运维和开发来说,是一个既基础又危险的漏洞。说它基础,是因为几乎所有web服务器和反向代理都会处理host头;说它危险,是因为一旦被利用,攻击者可以实施ssrf(服务器端请求伪造)、缓存投毒、密码重置令牌窃取等一系列攻击。很多团队在快速上线业务时,往往只关注功能实现,忽略了nginx这一层的关键安全配置,给攻击者留下了可乘之机。今天,我就结合这次实战排查和加固的过程,把如何配置nginx来有效防御http host头攻击的详细步骤、核心原理以及我踩过的坑,完整地分享出来。无论你是刚接触nginx的新手,还是希望完善现有架构安全性的资深工程师,这篇内容都能给你提供一份可直接“抄作业”的配置方案。
在动手修改nginx配置之前,我们必须先搞清楚敌人是谁,以及它会怎么攻击我们。只有理解了原理,配置起来才能心中有数,而不是机械地复制粘贴。
当你在浏览器地址栏输入 https://www.example.com 并按下回车时,浏览器发起的http请求中,一定会包含一个 host 请求头,其值就是 www.example.com 。这个头是http/1.1协议强制要求的,主要目的是让一台物理服务器(一个ip地址)能够托管多个域名(虚拟主机)。nginx、apache等web服务器就是根据这个 host 头的值,来决定将请求交给哪个 server 块(虚拟主机配置)来处理。
攻击者正是利用了这套机制的逻辑漏洞。如果我们的nginx配置不当,攻击者可以随意伪造这个 host 头。例如,他可能发送这样一个请求:
get / http/1.1 host: evil.com ...其他头部...
如果我们的nginx没有验证 host 头的合法性,它可能会:
我遇到的以及安全社区中常见的主要攻击场景有以下几种:
场景一:密码重置污染 这是危害最大的一种。许多web应用的密码重置功能会生成一个包含重置令牌的链接,并发送到用户邮箱。这个链接的生成方式可能是 https://$host/reset?token=xxx 。如果攻击者伪造 host: evil.com ,并且应用直接使用了这个值,那么用户收到的重置链接就会是 https://evil.com/reset?token=xxx 。一旦用户点击,令牌就泄露给了攻击者。
场景二:缓存投毒 在一些使用了缓存(如cdn、反向代理缓存)的场景中,缓存键(cache key)可能会包含 host 头。攻击者通过伪造 host 头,可以将恶意内容(如包含xss脚本的页面)注入到缓存中。当其他正常用户访问 https://www.example.com 时,cdn可能会返回被投毒的缓存内容,导致大规模的用户受影响。
场景三:服务端请求伪造(ssrf) 有些内部应用或api会根据 host 头来构造向后端服务的请求url。如果 host 头被篡改为内网地址(如 host: 169.254.169.254 ,这是云平台元数据服务的常见地址),可能导致应用向内部网络发起请求,窃取敏感信息。
场景四:绕过访问控制 某些应用可能会根据请求的域名来实施简单的访问控制逻辑。例如,只允许 admin.internal.example.com 访问管理接口。如果攻击者伪造 host: admin.internal.example.com ,而nginx又未加验证,请求就可能被错误地路由到后端应用,从而绕过域名层面的访问控制。
注意 :很多开发者和运维会有一个误区,认为使用了https或者配置了ssl证书就能防止host头篡改。这是完全错误的。https加密的是传输过程中的数据包内容,但请求头(包括host头)是应用层数据,客户端完全可以构造任意的host值发送过来。安全防护必须在服务端(nginx或应用自身)完成。
理解了攻击原理,我们的防护思路就清晰了:在nginx这一层,对进来的http请求的 host 头进行严格的校验和过滤,只放行我们明确允许的、合法的域名。同时,要确保传递给后端应用的主机名是可信的。我的整体设计思路分为三道防线:
下面,我们就沿着这三道防线,拆解具体的配置步骤。
在开始编写配置前,需要先理解nginx中几个关键的指令和内置变量:
我们的策略是:使用 server_name 列出所有合法域名,然后通过检查 $host 变量是否为空或不在合法列表内,来拦截非法请求。
接下来,我们进入实战环节。假设我们有两个合法的业务域名: www.example.com 和 api.example.com 。我们将为它们配置一个安全的nginx server 块。
首先,我们创建一个基础的服务器配置。关键点在于,我们不仅要在 server_name 中列出域名,还要主动检查 $host 变量。
server {
listen 80;
listen 443 ssl http2; # 如果启用https
server_name www.example.com api.example.com;
# ssl配置省略...
# --- 核心防护配置开始 ---
# 检查host头是否为空或不在允许的列表中
if ($host !~ ^(www\.example\.com|api\.example\.com)$) {
return 444; # 返回444状态码,nginx会直接关闭连接,不发送任何响应体。
# 也可以返回其他错误码,如403、400。
# return 403 "invalid host header";
}
# --- 核心防护配置结束 ---
location / {
proxy_pass http://backend_server; # 你的后端服务地址
# 其他proxy设置...
}
}
配置解析与注意事项:
上面的配置能处理匹配到本 server 块的请求。但如果请求的host头是 evil.com ,它可能根本不会进入这个 server 块,而是被nginx的默认规则处理。nginx会使用配置文件中 第一个 server 块作为默认服务器(对于listen指令相同的配置)。这很危险。
因此,我们必须显式定义一个默认的 server 块,监听相同的端口(80和443),但 server_name 设置为 _ (一个无效的域名占位符),并让它直接拒绝所有请求。
# 默认server块,必须放在文件最前面,或者确保是相同端口下的第一个server块。
server {
listen 80 default_server;
listen 443 ssl http2 default_server; # 同样处理https
server_name _; # 使用下划线作为占位符,匹配所有未在其他server块中定义的host
ssl_certificate /path/to/dummy.crt; # 可以指向一个自签或无效证书
ssl_certificate_key /path/to/dummy.key;
# 对于所有请求,直接返回错误或关闭连接
return 444;
# 或者记录日志后返回403
# access_log /var/log/nginx/invalid_host.log;
# return 403 "forbidden: invalid host";
}
# 然后才是你正常的业务server块,如上文的 www.example.com 配置
server {
listen 80;
listen 443 ssl http2;
server_name www.example.com api.example.com;
# ... 其余配置同上 ...
}
实操心得: 我强烈建议为默认 server 块配置一个独立的访问日志文件(如 invalid_host.log )。这样,所有被拦截的非法请求(包括扫描、攻击尝试)都会记录在这里,便于安全分析和监控,又不会污染正常业务日志。通过定期分析这个日志,你可以了解到你的服务器正在遭受哪些类型的host头攻击尝试。
通过了前两层检查,请求被认为是合法的。但在转发给后端应用(如 proxy_pass 到tomcat)时,我们还需要小心。默认情况下,nginx会将原始的 host 请求头原样转发给后端。为了绝对安全,我们应该主动设置转发时的 host 头,覆盖掉可能被污染的原值。
server {
listen 80;
server_name www.example.com api.example.com;
location / {
proxy_pass http://your_backend_upstream;
# 关键配置:覆盖转发给后端的host头
proxy_set_header host $server_name; # 使用当前匹配的server_name
# 或者更精确地,根据请求动态设置:
# proxy_set_header host $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;
# 建议也清除可能被滥用的其他头部
proxy_set_header x-forwarded-host ""; # 如果不需要则清空
}
}
为什么这样做? proxy_set_header host $server_name; 这行配置的意思是,无论客户端发来的 host 头是什么,nginx在转发请求给后端时,都会将其强制设置为当前 server 块匹配到的 server_name (例如 www.example.com )。这样就彻底切断了攻击者通过伪造host头来影响后端应用的可能性。后端应用收到的 host 头永远是我们预期的、合法的值。
重要提示 :有些后端应用可能依赖 host 头来生成链接或进行路由。使用 $server_name 可能过于静态(特别是当 server_name 有多个值时,它只取第一个)。在这种情况下,使用经过我们白名单校验的 $host 变量( proxy_set_header host $host; )是更灵活且同样安全的选择,因为它传递的是校验后的、原始的请求域名。
上面的配置适用于大多数场景。但在一些复杂架构下,可能需要更灵活的配置。
如果你的业务有大量子域名或泛域名需求,在 server_name 和 if 检查中使用正则表达式会更高效。
server {
listen 80;
# 使用正则匹配所有以 .example.com 结尾的域名
server_name ~^(?<subdomain>.+)\.example\.com$;
# 白名单校验也可以使用正则,或者更复杂的map指令
# 方法一:在if中使用正则(适用于简单模式)
if ($host !~ ^([a-z0-9]+\.)*example\.com$) {
return 444;
}
# 方法二(推荐):使用map指令定义白名单(更清晰,性能更好)
# 在http上下文中定义map
# http {
# map $host $valid_host {
# ~^(www|api|blog|shop)\.example\.com$ 1;
# ~^([a-z0-9-]+\.)*example\.com$ 1; # 谨慎使用泛匹配
# default 0;
# }
# }
# 然后在server块中:
# if ($valid_host = 0) {
# return 444;
# }
location / {
proxy_pass http://backend;
proxy_set_header host $host; # 对于泛域名,传递原始$host是合适的
}
}
使用 map 指令的优势 : map 指令在nginx启动时就将映射关系加载到内存中,查询效率是o(1)或o(log n)。而 if 中的正则表达式在每次请求时都需要编译和执行。当规则复杂或请求量大时, map 的性能优势明显,且配置更易于管理。
如果你有多台nginx服务器,或者使用了云负载均衡器(如aws alb、gcp lb),将host头校验放在最前端的入口点是最佳实践。这样,非法请求在进入内部网络之前就被丢弃了。
配置思路是一致的:在负载均衡器的监听器规则或健康检查配置中,确保只接受合法的 host 头。对于自建的nginx负载均衡器,配置方法与上述单机版完全相同。对于云服务商的lb,通常可以在控制台配置基于主机头的路由规则,将非法host的请求路由到一个返回错误的后端或直接丢弃。
配置写完了,千万别直接上生产环境。必须经过严格的测试。
使用 curl 命令可以非常方便地测试:
# 1. 测试合法请求(应该正常返回) curl -h "host: www.example.com" http://your_server_ip/ # 2. 测试非法请求(应该返回444/403或连接被关闭) curl -v -h "host: evil.com" http://your_server_ip/ # 观察响应状态码,如果是444,curl会报错:`curl: (52) empty reply from server` # 3. 测试没有host头的请求(应该被拒绝) curl -v http://your_server_ip/ # http/1.0请求默认无host头,也应被拦截 # 4. 测试通过ip地址直接访问(应该被默认server块拦截) curl -v http://your_server_ip/
在重启nginx前,务必使用 nginx -t 命令测试配置文件语法是否正确。
sudo nginx -t
如果显示 syntax is ok 和 test is successful ,才能进行下一步。
配置生效后,监控是持续保障安全的关键。
# 查看无效host日志中最高频的ip
sudo awk '{print $1}' /var/log/nginx/invalid_host.log | sort | uniq -c | sort -rn | head -20
在实际配置和运维中,你可能会遇到下面这些问题,这里我整理了排查思路和解决方法。
排查步骤:
add_header x-debug-host $host always;
解决方案:
排查步骤:
proxy_set_header x-original-host $http_host; # 将原始host头以其他名字转发
解决方案:
排查步骤: 如果你的负载均衡器(如kubernetes ingress, aws elb)或监控系统通过ip地址直接发起健康检查请求,这些请求通常没有 host 头,或者 host 头是ip地址,会被我们的防护规则拦截。
解决方案: 为健康检查单独开一个“绿色通道”。有两种常见方法:
location /health {
access_log off; # 可选,减少日志
# 不进行host校验
proxy_pass http://backend/health;
proxy_set_header host $host; # 仍然可以设置,或不设置
}
server {
listen 8080;
server_name _;
location /health {
proxy_pass http://backend/health;
}
}
在将配置部署到生产环境前,请对照此清单复查:
default_server 来捕获所有非法请求?server_name 是否列出了所有合法的业务域名(包括带www和不带www的)?if 语句或 map 指令中的正则表达式是否准确覆盖了所有合法域名,且排除了非法情况? proxy_set_header host ... 来净化转发给后端的请求? nginx -t 通过了语法测试? curl 等工具模拟了合法/非法请求进行测试?经过以上步骤,你的nginx服务器已经建立起了一道针对http host头攻击的坚实防线。这套配置的核心思想是“白名单”和“主动覆盖”,即在最外层只放行已知可信的,在向内传递时主动提供可信的值。
回顾整个配置过程,最关键的不是记住那几行配置代码,而是理解其背后的安全模型: 绝不信任客户端传来的任何数据,包括http头部 。host头攻击只是众多“请求头注入”攻击中的一种。同样的思路可以延伸到其他头部,比如:
最后,我想强调的是,nginx的配置是安全链条中的重要一环,但非唯一一环。后端应用程序自身也必须对接收到的host头进行二次验证(例如,与配置文件中允许的域名列表进行比对),实现纵深防御。同时,保持nginx和系统组件的及时更新,以修复可能出现的其他安全漏洞,才能构建起一个真正稳固的web服务防线。安全是一个持续的过程,配置只是开始,持续的监控、日志分析和应急响应同样重要。
到此这篇关于nginx防御http host头攻击实战的文章就介绍到这了,更多相关nginx防御http host头攻击内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
您想发表意见!!点此发布评论
版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。
发表评论