别全信 X-Forwarded-For:让 WAF 真正看到真实客户端 IP-第三维度MokIII

一个被忽略的默认值

很多 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;

关键点:

  1. set_real_ip_from 必须收敛到具体网段,不能再是 0.0.0.0/0;
  2. 用CDN 厂商约定并会覆盖的那个头,而不是通用的 X-Forwarded-For(后者客户端可以自己塞);
  3. 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 那个头"依然能冒充。

缓解办法:

  1. 源站防火墙只放行 CDN 回源段(安全组层面),让"直连源站"这条路根本不通;
  2. 如果源站规模不大,干脆只允许 CDN 的回源 IP 访问 80/443;
  3. 定期核对日志里出现的对端 IP 是否都落在 CDN 网段内。

这一条比改 header 更重要:改 header 只是让伪造更难,收敛来源才是让它不可能。

五、回滚

把 real_ip_header 改成原来的值(或删掉站点级覆盖)即可。但不要回滚到 set_real_ip_from 0.0.0.0/0——那是把问题请回来。


一句话总结:X-Forwarded-For 是客户端可以随便写的头,全信任它等于让 WAF 的封禁和限速全部失效。正确姿势是"只信任 CDN 网段 + 用 CDN 会覆盖的那个头",并且在防火墙层面把"直连源站"彻底堵死。