安全指南

X-Forwarded-For(XFF)伪造是什么:信任代理的配置错误与防御

X-Forwarded-For(XFF)是客户端可以伪造的 HTTP 头。从防御视角讲解在伪造 XFF 中放入注入探测的扫描、「信任所有代理」配置的问题,以及短期处理与只信任合适代理的根本对策。

发布于 2026-06-08 4 分钟阅读
本页目录

X-Forwarded-For(XFF)是即便经过代理,也用来告诉应用『真正的访客 IP 是这个』的头。然而这个值客户端可以随意伪造。本文把个人开发中常见的『遭到 XFF 伪造扫描』事例隐去专有名词,转化为教训(不提供攻击的复现步骤)。

事例摘要 — CASE FILE
种类
X-Forwarded-For 伪造 + 注入探测(自动扫描)
实际损害
无(被多层防御挡下)
发现方式
攻击请求变成 500,异常通知邮件大量飞来
生效的防护
①占位符(值的绑定)②DB 的字符编码校验
残留的配置错误
『信任所有代理』的配置(前端并无代理的架构)
处置
在边界对 IP 头做净化(短期)→ 修正信任代理的配置(根本)
0
实际损害(泄露・篡改)
2 道
生效的防护(多层防御)
全部允许
代理信任的配置
边界校验
主要对策

先做区分:是『攻击』还是『自己的失误』

看到错误通知不要立刻断定『是攻击』。如果刚刚做过架构变更,就应以同等分量去怀疑自己的改动。

1

怀疑自己的改动

先立一个假设:最近的配置变更导致用户反复重新登录。但这无法解释混入保存值里的非法字节序列 → 否决。
2

用值的内容作为证据

混入的是引号的多重 URL 编码、以及 overlong(冗余)字节序列。这不是正常用户或代理会生成的值 → 断定为恶意载荷。
3

是定向攻击还是扫描

与其说是针对特定目标,更可能是在整个互联网上无差别扫荡的自动扫描器,先这样判断。

本站的视角:从症状反推原因时,用数据逐一检验假设

『最近动过的地方可疑』这种直觉,作为出发点是有效的。但是,如果证据(这里是值的内容)予以否定,就要放手。立多个假设,不靠成见而用数据一个个排除,认错原因的情况就会大幅减少。

攻击的内容:在伪造的 XFF 里放入注入探测

攻击机器人在 XFF 里『以 IP 之名』塞入探测用字符串,只在末尾留一段像样的 IP。它要确认的只有一件事:

『这个应用,会不会相信 XFF 的值,把它生拼进 DB 查询、或原样输出到 HTML 里?』

如果应用做得马虎,就会经由 XFF 遭受 SQL 注入 或 XSS。这是把『承载 IP 的头』滥用为发送任意字符串的地方的手法。

为什么没有损害:两道防护

防护① 值的绑定(占位符)

没有把 IP 值做字符串拼接,而是用占位符把它作为数据交给 DB。输入不会被解释为指令 → SQLi 在结构上不成立。这是使用框架的主要好处之一。

防护② DB 的字符编码校验

载荷里的非法字节(overlong UTF-8 等),在列的字符编码下本就是无法保存的排列。DB 判定为『无效』,拒绝写入并抛出异常 → 攻击失败,而且被检测到了。

实际发生的只是『攻击者自己的请求变成 500,异常通知大量飞来』而已。泄露、篡改、非法登录一概没有。这是多层防御按预期生效的好例子。

真正的问题:把代理『全部』都信任了

如果到此为止就没有问题,但有一点不能放过。话说回来,伪造的 XFF 为什么能抵达 IP 判定?

✗ 信任全部代理(通配符)

客户端伪造的 XFF → 被当作『真实的 IP』采用 → 日志・会话・IP 判定都会用上伪造的值

✓ 什么都不信任(默认值)

忽略伪造的 XFF → 采用直接连接来源的真实 IP → 伪造失效

把信任代理设成通配符,谁伪造的 XFF 都会被当成『真实的 IP』通过。前端没有代理时,『什么都不信任』才是正解。

这个架构是既无反向代理也无 CDN、直连的 Web 服务器。也就是说根本没必要信任 XFF,却设成了『全部信任』。『先全部允许再说』,是开发时常见的设置原封不动留到生产环境的典型。

容易忽略:XFF 伪造的危害不只有『注入』

所有相信客户端 IP 的处理都会用上伪造的值。rate limit 的绕过、IP allowlist 的突破、BAN 规避、审计日志的伪造——即便注入防御很牢固,这也是另一个问题。请把『相信 IP 的地方』盘点出来,确认其信任的依据(=proxy 配置)。

对策:在边界净化 → 把信任收敛到合适范围

1

在边界对 IP 头做净化(短期・立即见效)

在 proxy 信任处理运行之前,把 XFF 切分,对每个元素以『是否为合法 IP』来校验,只保留合法的 IP。一个都没有就连头一起删除。正常用户的真实 IP 能通过校验所以不受影响,探测字符串会被丢弃。
2

正确设置信任代理(根本对策)

弃用通配符。前端没有代理就什么都不信任。proxy 信任还关系到 http/https 判定,改动可能带来重定向循环等副作用 → 先在一个环境里先行验证。
3

不要把『合法的 IP』与『真实的 IP』混为一谈

净化只是把格式整理好。形态正确的 IP 也可能是被伪造的值。真实 IP 的信任,始终靠②的 proxy 配置来保证。

验证:修好后复现攻击来确认

把『自以为修好了』变成『确实止住了』。①只在一个环境应用对策 → ②自己发送同样的非法头 → ③确认错误数不增加 → ④确认正常访问也照常工作 → ⑤没问题再推到全部环境。分阶段应用能防止事故。

续篇:IP 头被挡下后,攻击转到了 User-Agent

加上净化数十分钟后,又来了同一类错误。这次坏掉的是另一个列——不是 ip_address 而是 user_agent。IP 头被挡下的攻击者,把同样的恶意载荷换到了 User-Agent 头上。这需要换一种做法。

入口:XFF・User-Agent・Referer・… (多到数不清)
↓ 其中『会被保存』的是
出口:sessions 表里的 ip_address 与 user_agent 这两个而已
↓ 所以该净化的入口是
只需通向那两个的头(IP 系 + User-Agent)即可
一个个去堵危险的头,攻击只会转到别的头上。从不可信的值最终被保存到哪里(出口)反推,需要处理的入口就成了一份有限的清单。

正确的问题不是『哪个头危险』,而是『不可信的值最终会被保存到哪个 DB 列』。如果保存目标只有 ip_address 和 user_agent 这两个,那么只要净化通向这两个的入口(IP 系头+User-Agent,保险起见连 Referer 也加上),源自会话的错误在原理上就能止住;把载荷换到其他不会被保存的头上,也不会引发错误。正常的 User-Agent 是 ASCII/正常的 UTF-8,所以加上只剔除无效字节的处理也毫无影响。

本站的视角:从出口反推,而不是一个个去堵入口

一个个去堵头是行不通的,攻击者总能换用别的头。从出口(保存列・输出位置)反推,把通向那里的入口一次全部梳理出来才是正解。正如这次攻击从 IP 头转到 User-Agent 所显示的,在处理某个头时,要把抵达同一保存目标的其他入口一并处理。而且第二次同样在复现并确认止住后才推开。

再续篇:在保存前一刻进行防御

把 User-Agent 也处理好之后紧接着,ip_address 列又来了同样的错误。入口的净化已经部署,用自己发送与攻击相同的非法头来测试也无法复现。可在生产上却反复出现——这是这个事例的关键。

原因在于,框架的『客户端 IP 判定』内部很复杂。多种头的写法(X-Forwarded-For・Forwarded・自定义头)、与 proxy 信任配置的组合、解析顺序与缓存……在这些内部路径的某处,恶意的值从我们没有净化过的另一条路线,又作为『客户端 IP』出现了。只在入口检查无法全部拦住。

✗ 在入口守

净化请求的头 → 恶意的客户端 IP 经内部另一条路线再次出现 → 被绕过

✓ 在出口守

在 DB 保存的前一刻检查 → 所有值都必须经过的地方 → 无法绕过

入口(对进来的值做检查)可能被内部的解析路径绕过。出口(写入 DB 的前一刻)是所有值都必须经过的地方,所以不会被绕过。

于是改变做法,决定在出口(写入 DB 的前一步)守住。继承会话保存类,只替换生成保存值的那部分(概念上):保存用 IP『若非合法 IP 则为 null』,保存用 User-Agent『剔除非法字节并限制长度』。无论客户端 IP 判定在内部怎样运作,写入 DB 的前一刻都一定会检查,所以无从绕过。把恶意头沿两条攻击路线各发送十几次,错误也没有增加,正常用户的会话也一切正常。

本站的视角:最终检查应放在出口(使用的地方)

安全的基本原则是『不要相信输入,在使用的地方(出口)务必校验・转义』。入口的检查在『尽早拦截』上有效,但对于框架会在内部解析・加工的那类信息(客户端 IP 判定等),它可能绕过入口检查而再次出现。在传给 DB・HTML・命令等危险位置的前一刻,是所有值都必须经过的地方,所以最终的校验・转义应放在这里。入口与出口是两回事,两者都要做。

教训:带着明显错误被拒绝更安全

这一次,攻击不仅失败,还以大量错误通知的形式显现出来。这并不是坏结果:比起不知不觉地被放行,带着明显的错误被拒绝更安全。不要把错误通知当成噪声丢掉,而要用它来察觉异常——不止步于『实际损害已防住』,而是去修复『为什么会一路抵达内部』,这才是本站的立场。

接下来阅读

FAQ

QX-Forwarded-For 可以信任吗?
A

原样是不能信任的。XFF 是客户端可以随意填值的头,攻击者既能伪造成看似真实的 IP,也能填入注入探测的字符串。可以信任的,只有『自己放在前端、由信任代理附加的 XFF』。

Q没有使用反向代理或 CDN 时该怎么办?
A

完全不信任 XFF(保持框架的默认值)。前端并没有代理却设成『信任所有代理』,谁都能把伪造的 IP 传给应用。没有相应架构时,什么都不信任才是正解。

Q对 IP 头做净化就安全了吗?
A

注入探测和大量错误会被止住,但那只是短期处理。即便能整理成『合法的 IP』,也不保证它就是『真实的 IP』。真实 IP 的信任,始终要靠信任代理的配置来保证。

Q明明改好了 IP 头,怎么又冒出同样的错误?
A

因为同一个恶意载荷被换到了另一个头(例如 User-Agent 头),最终又抵达了同一个保存列。一个个去堵头,攻击者只会换到别的头上。请从『不可信的值最终会抵达哪个 DB 列/输出位置』反推,把通向那里的入口(IP 系+User-Agent 等)一并净化。