阅读提要
直接读 X-Forwarded-For 的第一个值,等于让客户端自报身份。限流、封禁、审计全部建立在「IP 可信」上,这一步做错,后面全部失真。
问题从哪来
加了 Nginx 或 Caddy 之后,应用看到的 remoteAddress 全是 127.0.0.1。访问日志、限流、封禁全部失效。
于是大家都在代码里读 X-Forwarded-For:
const ip = req.headers['x-forwarded-for'].split(',')[0]
这个写法有两个问题:客户端可以伪造这个头,以及多级代理时第一个值不一定是客户端。

X-Forwarded-For 是怎么被追加的
代理转发请求时,会把「上一跳的地址」追加到现有头的末尾。
客户端 1.2.3.4 → CDN → Nginx → 应用
到达应用时,头可能是:
X-Forwarded-For: 1.2.3.4, 203.0.113.9
其中 1.2.3.4 是原始值(客户端发的,可能是伪造的),203.0.113.9 是 CDN 的地址(可信代理追加的)。
关键:只有可信代理追加的那一段可信。左侧的值全部可以被客户端伪造。
正确算法:从右往左
const TRUSTED = new Set(['127.0.0.1', '::1', '10.0.0.0/8'])
function realIp(req) {
const chain = String(req.headers['x-forwarded-for'] || '')
.split(',')
.map(item => item.trim())
.filter(Boolean)
chain.push(req.socket.remoteAddress || '')
// 从右往左,跳过可信代理,第一个不可信的就是客户端
for (let i = chain.length - 1; i >= 0; i--) {
if (!isTrusted(chain[i])) return chain[i]
}
return chain[0] || '0.0.0.0'
}
逻辑是:最右边一定是直连你的那一方(可信代理),往左依次跳过所有可信代理,第一个不可信的就是真实客户端。
这样构造的请求 X-Forwarded-For: 8.8.8.8(伪造)到达应用时,链是 ['8.8.8.8', '203.0.113.9'],从右往左第一个不可信的是 203.0.113.9(CDN),结果为 CDN 的地址——不是伪造的 8.8.8.8。攻击者无法通过伪造头绕过限流。
各框架的处理方式
Nginx 可以自己算好,用 real_ip 模块:
set_real_ip_from 203.0.113.0/24; # 信任的 CDN 段
real_ip_header X-Forwarded-For;
real_ip_recursive on;
proxy_set_header X-Real-IP $remote_addr;
之后应用直接读 X-Real-IP 就行——但这个头只能来自 Nginx,不能直接信任外部的同名头。
Express 有内置支持:
app.set('trust proxy', ['loopback', '10.0.0.0/8'])
设置之后 req.ip 就是正确值。trust proxy 设成 true 是危险的——它会信任任何代理头。
Nuxt/Nitro 类似,需要显式配置信任的代理层。
别忘了 IPv6 和端口
remoteAddress 在 IPv6 下是 ::ffff:1.2.3.4 这种格式,直接存库会导致同一个客户端被算成两个。
function normalize(ip) {
return ip.startsWith('::ffff:') ? ip.slice(7) : ip
}
另外 X-Forwarded-For 里可能带端口(1.2.3.4:53214),取 IP 时要剥离。
这个错误会导致什么
- 限流失效:所有人共享代理的 IP,一被限流全体用户受影响;或者攻击者伪造头,永远触发不了限流;
- 封禁误伤:封了一个 IP,实际封的是整个 CDN 节点;
- 日志失真:访问统计、地区分析、异常检测全部建立在错误数据上;
- 安全审计失效:出事时查不到真正的来源。
第二条最严重——你封禁了 CDN 的地址,等于把站点对所有用户关闭。
自检方法
上线前用 curl 从外部构造一个伪造头:
curl -H "X-Forwarded-For: 1.2.3.4" https://your.site/api/echo-ip
返回的应该是真实的出口 IP(你的宽带地址),不是 1.2.3.4。如果返回了 1.2.3.4,说明代码信任了客户端输入。
这一步必须做。它是唯一能确认「伪造无效」的方法。
收尾
真实 IP 这件事,本质是建立信任边界:哪些跳转是你能控制的,哪些不是。
规则一句话:只信任你自己部署的代理追加的头,从右往左读,其余一律不认。
你认为这篇文章怎么样?
还没有表情反馈

一起聊聊
一个想法、一点补充,或一句简单的问候。
评论与回复
友善交流,彼此尊重第一句话,留给你。
读完有什么感受?欢迎成为第一个分享的人。