跳到正文
>_ITDITDWeb 安全平台

安全指南

X-Forwarded-For(XFF)伪造是什么 — 信任代理配置的陷阱与防御

X-Forwarded-For(XFF)是客户端可以随意伪造的 HTTP 头。本文以个人开发中常见的事例,从防御视角讲解:在伪造的 XFF 里夹带 SQL 注入或 XSS 探测的自动扫描,以及『信任所有代理』这一配置的陷阱(不提供攻击步骤)。多层防御为什么有效、即便如此还有什么本该修复,将分成对症处理与根本对策来总结。

发布于 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。输入没有变成 SQL『指令』的余地 → 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_agentIP 头被挡下的攻击者,把同样的恶意载荷换装到了 User-Agent 头上。这里需要转换思路。

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

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

本站的视角:从出口反推(不要一个个去堵入口)

一个个去堵头是打地鼠——攻击者能用的入口多到数不清。从出口(保存列・输出位置)反推,把通向那里的入口一次全部梳理出来才是正解。正如『堵了 IP,又被戳了 User-Agent』所学到的,做对症处理去堵某个头时,要把抵达同一保存目标的其他入口一并堵上。而且第二次同样在复现并确认止住后才推开。

更进一步的续篇:入口没能完全挡住,靠出口(保存前一刻)守住了

把 User-Agent 也堵上、心想『这下没问题』的紧接着,ip_address 列又来了同样的错误。入口的净化已经部署,用自己发送与攻击相同的非法头来测试也无法复现。可在生产上却复发——这里是这个故事的高潮。

原因在于,框架的『客户端 IP 判定』内部很复杂。多种头的写法(X-Forwarded-ForForwarded・自定义头)、与 proxy 信任配置的组合、解析顺序与缓存……在这些内部路径的某处,污染值从与我们净化过的值不同的另一条路线作为『客户端 IP』复活了。在入口无法全部捕获

✗ 在入口守

净化请求的头 → 客户端 IP 经内部另一条路线复活 → 被绕过

✓ 在出口守

在 DB 保存的前一刻检查 → 无论中途怎样被污染都是『必经的一点』 → 无法绕过

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

于是把思路掉转 180 度,决定在出口(写入 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 等)一并净化。