客户端 IP 伪造漏洞分析与修复实践
客户端 IP 是日志审计、接口限流、风控策略、黑白名单等安全能力的重要基础,但很多项目在获取客户端 IP 时都会直接信任 `X-Forwarded-For` 请求头,导致攻击者可以通过伪造请求头绕过 IP 限流、规避黑名单、污染审计日志,甚至影响安全事件溯源。本文将从 `X-Forwarded-For` 的工作原理出发,分析客户端 IP 伪造漏洞的成因及风险,并结合 Nginx 与 Spring Boot 的实际部署场景,介绍如何构建可信的 IP 获取链路,避免因错误使用代理请求头而引入安全隐患。
前言
在业务开发中,我们经常需要获取客户端 IP,用于:
- 登录日志
- 操作审计
- 风控策略
- 接口限流
- 黑白名单
- 地域限制
- IP 归属地查询
很多项目都会封装一个 IpUtil.getIpAddr() 工具,然后统一使用。
然而,不少项目都存在一个容易被忽略的问题:
直接信任了 X-Forwarded-For 请求头。
攻击者无需突破任何权限,仅通过构造一个 HTTP Header,就可以伪造自己的来源 IP,导致整个风控体系失效。
本文结合实际案例,分析这一漏洞产生的原因,以及如何彻底解决。
一个十分常见的错误实现
很多项目都会采用类似下面的代码:
String ip = request.getHeader("X-Forwarded-For");
if (ip == null) {
ip = request.getRemoteAddr();
}
if (ip.contains(",")) {
ip = ip.substring(0, ip.indexOf(","));
}看起来没有任何问题。
甚至很多开源项目也是这样写的。
但是它隐藏着一个安全漏洞。
X-Forwarded-For 到底是什么?
HTTP 协议本身并不会告诉服务端:
「这个请求最初来自哪个客户端。」
真正建立 TCP 连接的,只能看到上一跳。
例如:
Client
│
▼
CDN
│
▼
SLB
│
▼
Nginx
│
▼
Spring Boot
Spring Boot 实际建立 TCP 连接的是:
Nginx
因此:
request.getRemoteAddr()
获取到的是:
Nginx IP
而不是用户 IP。
于是代理服务器会增加一个 Header:
X-Forwarded-For
例如:
X-Forwarded-For:
1.2.3.4
表示:
真正的客户端 IP 是:
1.2.3.4
多级代理时:
Client
↓
CDN
↓
SLB
↓
Nginx
↓
Java
Header 会变成:
X-Forwarded-For:
1.2.3.4, 100.10.0.1, 172.20.0.10
其中:
第一个 IP
1.2.3.4
就是客户端。
所以很多代码都会:
取第一个 IP
真正的问题来了
很多人认为:
X-Forwarded-For 就是真实客户端 IP。
实际上并不是。
因为:
HTTP Header 可以由客户端自己构造。
攻击者完全可以发送:
GET /api/login HTTP/1.1
Host: example.com
X-Forwarded-For: 8.8.8.8或者:
X-Forwarded-For:
114.114.114.114甚至:
X-Forwarded-For:
127.0.0.1如果后端代码:
request.getHeader("X-Forwarded-For")直接相信它。
那么日志中记录的 IP 就会变成:
114.114.114.114
而实际上:
攻击者可能来自:
45.xx.xx.xx
这就是 客户端 IP 伪造(Client IP Spoofing)。
漏洞会造成哪些影响?
这个漏洞影响远比想象的大。
1. IP 限流失效
例如:
每 IP 每分钟:
10 次
攻击者不断修改 Header:
X-Forwarded-For:
1.1.1.1
2.2.2.2
3.3.3.3
系统认为:
都是不同用户。
限流完全失效。
2. 黑名单绕过
例如:
Redis:
BLOCK_IP
45.xx.xx.xx
攻击者:
X-Forwarded-For:
8.8.8.8
直接绕过。
3. 风控失效
例如:
异地登录:
北京
↓
一分钟后
广州
实际上:
攻击者一直在国外。
只是不断修改 Header。
导致风控判断错误。
4. 审计日志失真
后台看到:
管理员登录
IP:
192.168.1.1
实际上:
日志已经被污染。
后续无法追踪真实来源。
5. IP 归属地错误
例如:
上海
北京
广州
全部可以伪造。
影响:
- 数据分析
- 用户画像
- 风险模型
为什么很多项目都会踩这个坑?
原因其实很简单。
大家误以为:
X-Forwarded-For
=
服务器生成
实际上:
它只是一个普通 Header。
客户端完全可以发送。
只有:
可信代理
覆盖后的 Header 才可信。
正确的处理方式
真正安全的做法遵循一个原则:
不要信任客户端,只信任可信代理。
也就是说:
只有请求来自:
Nginx
SLB
CDN
API Gateway
这些可信代理时,
才去解析:
X-Forwarded-For
否则:
直接使用:
request.getRemoteAddr()正确的整体架构
Client
│
▼
CDN
│
▼
SLB
│
▼
Nginx
│
▼
Spring Boot
处理流程:
先获取
RemoteAddr
↓
判断:
是不是可信代理
↓
否
↓
直接返回 RemoteAddr
↓
是
↓
解析 X-Forwarded-For
↓
取第一个合法公网 IP
整个信任链如下:
客户端
↓
(不可信)
HTTP Header
↓
Nginx
↓
(可信)
Spring Boot
而不是:
客户端
↓
Spring Boot
直接相信 Header。
Nginx 也必须做好配置
Java 代码安全,并不代表系统安全。
代理层同样需要规范 Header。
例如:
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;这样:
后端读取到的:
X-Forwarded-For
来自代理。
而不是客户端直接传入。
如果前面还有 CDN、SLB,同样需要保证它们不会直接透传客户端伪造的请求头,而是按照代理链重新生成或规范追加 X-Forwarded-For,形成完整且可信的 IP 传递链。
Java 侧最佳实践
Java 层建议遵循以下原则:
- 默认使用
request.getRemoteAddr()获取连接来源。 - 仅当
RemoteAddr属于可信代理(Nginx、SLB、API Gateway 等)时,才解析X-Forwarded-For或X-Real-IP。 - 从
X-Forwarded-For中提取第一个合法的客户端公网 IP。 - 忽略
unknown、空值及非法格式。 - 不将内网地址、回环地址作为真实客户端 IP。
- 统一封装为工具类,避免业务代码自行解析请求头。
总结
如果直接相信客户端传入的 X-Forwarded-For,攻击者可以轻松伪造来源 IP,进而绕过限流、黑名单、风控策略,污染审计日志,甚至影响安全事件溯源。
真正安全的实现并不是简单地读取某一个 Header,而是建立一套完整的信任模型:
- 客户端永远不可信。
- 只有可信代理生成的 Header 才可信。
- 应用层必须结合代理链判断是否解析转发头。
- 代理层和应用层共同构建完整的 IP 信任链。
只有这样,获取到的客户端 IP 才能真正作为安全策略、审计日志和风控系统的可靠依据。