文章总结: 本文解析Nginx反向代理后后端日志显示内网IP的原因,指出X-Forwarded-For与X-Real-IP可被客户端伪造,不可直接信任。标准解法是使用ngxhttprealipmodule模块,通过setrealipfrom声明可信代理网段,配合realipheader与realiprecursive指令正确提取真实客户端IP,并给出配置示例与验证方法。 综合评分: 85 文章分类: 安全运维,解决方案,WEB安全
X-Forwarded-For 能伪造,Nginx 反代怎么拿到真实 IP?
原创
浮光入梦 浮光入梦
浮光入梦
2026年9月12日 09:09 北京
在小说阅读器读本章
去阅读
在公众号小说中沉浸阅读
后端同学找你:”线上接口被刷了,看下 IP 我去封。”你一翻 access.log,整个人愣住了——127.0.0.1 一片一片的,偶尔夹几条 10.0.0.5,全是内网段,哪有真实客户端的影子?
这不是后端程序写错了,是反向代理这套机制本身的”特性”。搞清楚真实 IP 是怎么丢的、以及怎么拿回来,是每个运维都要过一遍的坎。
现象:后端日志里全是 127.0.0.1
先看一眼最常见的 Nginx 反向代理配置:
server {
listen 80;
server_name api.example.com;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
很多同学一看这配置觉得没毛病——X-Real-IP 也写了、X-Forwarded-For 也写了,应该 OK 啊。但后端 Tomcat、PHP-FPM、Go、Node 这些服务读 socket 拿到的连接来源是 Nginx——TCP 连接是 Nginx 主动发起的,$remote_addr 在 Nginx 这里指”对端的 IP”,也就是 Nginx 自己看到的客户端 IP;但对后端来说,它看到的”对端”是 Nginx,所以 socket 层的来源地址是 127.0.0.1 或 Nginx 所在的内网 IP。
HTTP 头是字符串,后端要拿真实 IP,要么从 socket 层(拿到的是 Nginx 的 IP),要么去读 Nginx 传过来的头。X-Real-IP 和 X-Forwarded-For 就是为了解决”怎么把客户端真实 IP 透传到后端”而存在的。
X-Real-IP 和 X-Forwarded-For:两个最常见的”伪真实 IP”
这俩头长得像、都在传递客户端 IP、网上教程也经常混着用,但本质完全不同。
X-Real-IP 是 Nginx 社区的”民间约定”,不是任何 RFC 标准。最早在 nginx 的 proxy 模块里被广泛使用,约定俗成就成了事实做法。它的语义很简单:只传一个 IP,就是”我自己看到的对端”。proxy_set_header X-Real-IP $remote_addr; 这行就是把 Nginx 自己看到的客户端 IP 塞进去。单层代理的场景下这就够用了。
X-Forwarded-For (XFF) 是事实标准,IETF 在 RFC 7239 里把它正式标准化成了 Forwarded 头(语法略有差别,但 XFF 因为太流行保留了下来)。它的语义是一个 IP 列表:每经过一层代理,就把自己看到的对端 IP 追加到列表末尾,用逗号分隔。所以 XFF 的值看起来像 client_ip, proxy1_ip, proxy2_ip,最后一个 IP 是”最近一跳代理看到的对端”。
Nginx 里要用专门的内置变量 $proxy_add_x_forwarded_for,它的语义是:请求里已经有 XFF 就保留,没有就用空字符串;然后把当前 $remote_addr 追加到末尾(中间用逗号连接)。所以 $proxy_add_x_forwarded_for 是”追加”而不是”覆盖”,这个细节很多人忽略。
逐维对比一下:
| 维度 | X-Real-IP | X-Forwarded-For |
| — | — | — |
| 是否 RFC 标准 | 否,Nginx 社区约定 | 事实标准,RFC 7239 标准化为 Forwarded |
| 传几个 IP | 一个 | 一个列表(逗号分隔) |
| 语义 | “我自己看到的对端” | 客户端 → 代理链 |
| 多层代理行为 | 只反映最近一跳 | 完整保留整条链 |
| Nginx 变量 | $remote_addr | $proxy_add_x_forwarded_for (追加) |
| 是否可被客户端伪造 | 是 | 是(更危险) |
别瞎信:这两个头,客户端一句话就能伪造
不管是 X-Real-IP 还是 X-Forwarded-For,本质都是 HTTP 请求头,而 HTTP 请求头是客户端完全可控的。你拿 curl 一行就能伪造:
curl -H "X-Forwarded-For: 1.2.3.4" -H "X-Real-IP: 1.2.3.4" http://api.example.com/
如果后端代码直接 request.getHeader("X-Forwarded-For").split(",")[0] 拿第一个 IP 就当真实客户端,那攻击者想伪装成哪个 IP 就能伪装成哪个 IP。限流按 IP 限?直接绕过。日志按 IP 审计?直接造假。
危险操作警告:把 XFF/X-Real-IP 当成可信的客户端标识、用来做安全决策,是反代架构里最经典的坑。任何写”取 XFF 第一个 IP”的代码都必须配套一层”这个头是不是从可信代理传过来的”的判断。
Nginx 自己设计的反代头在传递链路上是可信的(因为 Nginx 是我们自己控制的),但客户端直接发的 XFF 是不可信的。要区分这两者,必须在 Nginx 侧把”可信代理”和”不可信客户端”明确分开——这就是 ngx_http_realip_module 干的事。
拿到真实 IP 的标准做法:realip 模块
ngx_http_realip_module 是 Nginx 官方模块,默认编译进 Nginx(编译参数是 --with-http_realip_module;除非显式 --without-http_realip_module 才会被去掉)。验证一下:
nginx -V 2>&1 | grep -o 'with(out)?-http_realip_module'
这个模块做一件事:把 $remote_addr 在 Nginx 内部改写成”我们认为的客户端真实 IP”,改写之后后端再读 socket 或者日志里看到的都是改写后的值。
标准配置长这样:
server {
listen 80;
server_name api.example.com;
# 信任的反代上游网段(前一跳代理的 IP 段)
set_real_ip_from 10.0.0.0/8; # 内网 Nginx 集群
set_real_ip_from 192.168.0.0/16; # 其他内网段
# set_real_ip_from 203.0.113.5; # 某个具体的代理 IP 也支持
# 从哪个头里取真实 IP
real_ip_header X-Forwarded-For;
# real_ip_header X-Real-IP; # 如果只用 X-Real-IP 就写这个
# 多层代理时是否递归查找(见下节)
real_ip_recursive on;
location / {
proxy_pass http://127.0.0.1:8080;
# 透传给后端的头可以保留,方便后端调试
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
核心就两个指令:
set_real_ip_from:列出”哪些来源是可信代理”。只写 Nginx 自己集群的网段,绝不写0.0.0.0/0或者客户端公网网段,否则任何人伪造 XFF 都能骗过你。real_ip_header:告诉 Nginx 从哪个头里读真实 IP。默认值是X-Real-IP,但绝大多数场景下你写的是X-Forwarded-For(因为 XFF 是事实标准,多层代理都保留)。real_ip_recursive:多层代理时才用到,下一节细讲。
配上之后,$remote_addr 在这个 server 里就变成了”真实客户端 IP”,日志和反代头里的值都跟着改。后端直接读 socket 来源就行,不用再去 split 字符串。
多层代理与 CDN:real_ip_recursive 才是关键开关
单层 Nginx → 后端这种简单场景,real_ip_recursive 写 off(默认值)就行:Nginx 找到 XFF 列表里”第一个不是可信代理的 IP”,就把它当作真实 IP。
但生产环境几乎都是多层:客户端 → CDN → Nginx → 后端。这时候 XFF 列表里至少有三段:客户端真实 IP(CDN 追加的)、CDN 边缘节点 IP、Nginx 自己追加的内网 IP。
real_ip_recursive off(默认)的行为:取 XFF 列表从右往左数,第一个不在 set_real_ip_from 网段里的 IP 作为真实 IP。如果你的 set_real_ip_from 写了 CDN 的回源网段,那”从右往左数第一个不在网段里”就是 CDN 节点 IP——不是真实的客户端 IP。
real_ip_recursive on 的行为:从右往左连续跳过所有在 set_real_ip_from 网段里的 IP,直到撞上一个不在网段里的 IP 才是真实客户端。所以 CDN 场景必须 on:
# CDN(以 Cloudflare 为例)的回源 IP 段
# 实际值以 CDN 官方文档为准,常见做法是写一份可维护的列表
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 103.21.244.0/22;
set_real_ip_from 103.22.200.0/22;
set_real_ip_from 103.31.4.0/22;
set_real_ip_from 141.101.64.0/18;
set_real_ip_from 108.162.192.0/18;
set_real_ip_from 190.93.240.0/20;
set_real_ip_from 188.114.96.0/20;
set_real_ip_from 197.234.240.0/22;
set_real_ip_from 198.41.128.0/17;
set_real_ip_from 162.158.0.0/15;
set_real_ip_from 104.16.0.0/13;
set_real_ip_from 104.24.0.0/14;
set_real_ip_from 172.64.0.0/13;
set_real_ip_from 131.0.72.0/22;
# 加上自己的 Nginx 集群网段
set_real_ip_from 10.0.0.0/8;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
危险操作警告:CDN 的回源网段是会变的,必须按官方文档定期更新。
set_real_ip_from一旦漏配,CDN 节点的 IP 会被当成”客户端”,下游看到的所有真实用户 IP 全错。
改完配置一定要验证。 最快的办法是在 log_format 里同时打印改写前和改写后的值,对比看:
log_format realip_log '$remote_addr - $realip_remote_addr [$time_local] '
'"$request" $status "$$http_x_forwarded_for"';
access_log /var/log/nginx/access.log realip_log;
$remote_addr 是改写后的值(你期望的真实客户端 IP),$realip_remote_addr 是改写前的值(也就是上一跳代理的 IP,正常应该是 CDN 节点或内网 Nginx IP)。$http_x_forwarded_for 是请求里原始的 XFF 字符串,方便排查伪造情况。
写一段小脚本快速验证:
# 用 curl 伪造一个 XFF,看看 Nginx 拿到的是不是被忽略了
curl -H "X-Forwarded-For: 1.2.3.4" http://api.example.com/
tail -n 1 /var/log/nginx/access.log
# 期望:$remote_addr 显示的是你自己的真实出口 IP,不是 1.2.3.4
反代后拿不到真实 IP,十有八九是 set_real_ip_from 没配或者 real_ip_recursive 没开。日志里看到的真实客户端 IP 全是 CDN 节点或内网 Nginx IP,几乎可以肯定是 CDN 回源网段漏配。
搞清楚 XFF 是怎么被追加、X-Real-IP 为什么只传一个,再用 realip 模块把”可信代理”圈出来,真实 IP 这件事才算落地。下次再有”后端日志全是 127.0.0.1″的工单,就知道从哪儿下手了。
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:浮光入梦 浮光入梦 浮光入梦《X-Forwarded-For 能伪造,Nginx 反代怎么拿到真实 IP?》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。









评论