
一个被忽略的默认值
很多 nginx 配置(含一些面板的默认模板)里写着:
set_real_ip_from 0.0.0.0/0;
real_ip_header X-Forwarded-For;
翻译一下:任何来源的请求,只要带一个 X-Forwarded-For 头,nginx 就把它当成真实客户端 IP。
在"源站前面有 CDN"的场景下,这么写看起来很方便——不用管 CDN 的网段。但它有一个直接后果:任何人只要自己加个 X-Forwarded-For 头,就能伪装成任意 IP。
而 WAF、限速、封禁、人机验证,全都是按"这个 IP"来计数的。于是:
- 攻击者每换一个假 IP,就重新获得一次配额;
- 你的封禁列表封的是一堆假 IP;
- 日志里全是伪造地址,事后根本没法溯源。
一、我是怎么发现的
起因是排查访问日志:日志最后 2000 行里,单个 IP 出现了 1179 次——高频抓取。但把它加进封禁后,抓取并没有停。
一查配置才明白:那份"高频 IP"本身就是伪造出来的。真正的客户端一直在换 X-Forwarded-For,而我按它封,等于封空气。
二、正确的做法
分两种情况。
情况 A:源站前面有 CDN,且 CDN 会写入真实 IP
只信任 CDN 的回源网段,并且改用 CDN 自己写的那个头。以阿里云 ESA 为例,它会写入 ali-real-client-ip,并且会覆盖客户端伪造的同名头:
# 全局:只信任回源网段
set_real_ip_from 127.0.0.1;
set_real_ip_from <CDN 回源网段1>;
set_real_ip_from <CDN 回源网段2>;
# 站点级:用 CDN 写入的头
real_ip_header ali-real-client-ip;
关键点:
set_real_ip_from必须收敛到具体网段,不能再是0.0.0.0/0;- 用CDN 厂商约定并会覆盖的那个头,而不是通用的
X-Forwarded-For(后者客户端可以自己塞); real_ip_recursive的语义要想清楚(默认 off 时取的是 XFF 最右边一个),不确定就别开。
情况 B:源站直连公网,没有 CDN
那就别信任任何转发头——删掉 real_ip_header,让 nginx 直接用 TCP 连接的对端地址。这也是最安全的默认。
三、验证伪造是否还成立
# 伪造一个 XFF,看日志里记的是伪造值还是真实值
curl -s -o /dev/null -H 'X-Forwarded-For: 9.9.9.9' https://www.example.com/
tail -1 /www/wwwlogs/example.com.log | awk '{print $1}'
- 打印出
9.9.9.9→ 伪造成功,配置有问题; - 打印出你的真实出口 IP → 收敛生效。
这一步很有必要,"看配置文件"和"实测"经常不一致。
四、诚实的边界:仍有残留风险
必须说清楚:如果 CDN 用的是回源时不带固定网段的形态(部分免费套餐就是如此),那么"直连源站 + 手动伪造 CDN 那个头"依然能冒充。
缓解办法:
- 源站防火墙只放行 CDN 回源段(安全组层面),让"直连源站"这条路根本不通;
- 如果源站规模不大,干脆只允许 CDN 的回源 IP 访问 80/443;
- 定期核对日志里出现的对端 IP 是否都落在 CDN 网段内。
这一条比改 header 更重要:改 header 只是让伪造更难,收敛来源才是让它不可能。
五、回滚
把 real_ip_header 改成原来的值(或删掉站点级覆盖)即可。但不要回滚到 set_real_ip_from 0.0.0.0/0——那是把问题请回来。
一句话总结:X-Forwarded-For 是客户端可以随便写的头,全信任它等于让 WAF 的封禁和限速全部失效。正确姿势是"只信任 CDN 网段 + 用 CDN 会覆盖的那个头",并且在防火墙层面把"直连源站"彻底堵死。

评论 (0)