bigywater.tech
Gin · 48

ClientIP 与 TrustedProxies:X-Forwarded-For 的信任链

日志和限流里的客户端 IP 可信吗?默认配置下,任何人加一个 X-Forwarded-For 头就能改写 c.ClientIP()。这篇用伪造的请求把 Gin 取客户端 IP 的规则逐项验证:默认值、SetTrustedProxies、多层代理、头的优先级,以及 TrustedPlatform 的风险。

1.2.3.4
默认配置下直连客户端伪造 X-Forwarded-For: 1.2.3.4 得到的 ClientIP(真实地址 203.0.113.9)
203.0.113.7
设置受信任代理后,XFF 为 9.9.9.9, 203.0.113.7 得到的 ClientIP——伪造的最左项被丢弃
<script>
TrustedPlatform 下 CF-Connecting-IP 写成 <script> 时被原样返回为 ClientIP

取 IP 的流程

实测 · 本机执行输出
  context.go 的 ClientIP(Gin v1.12.0):
     975 | func (c *Context) ClientIP() string {
     976 |     // Check if we're running on a trusted platform, continue running backwards if error
     977 |     if c.engine.TrustedPlatform != "" {
     978 |         // Developers can define their own header of Trusted Platform or use predefined constants
     979 |         if addr := c.requestHeader(c.engine.TrustedPlatform); addr != "" {
     980 |             return addr
     981 |         }
     982 |     }
     983 | 
     984 |     // Legacy "AppEngine" flag
     985 |     if c.engine.AppEngine {
     986 |         log.Println(`The AppEngine flag is going to be deprecated. Please check issues #2723 and #2739 and use 'TrustedPlatform: gin.PlatformGoogleAppEngine
     987 |         if addr := c.requestHeader("X-Appengine-Remote-Addr"); addr != "" {
     988 |             return addr
     989 |         }
     990 |     }
     991 | 
     992 |     var (
     993 |         trusted  bool
     994 |         remoteIP net.IP
     995 |     )
     996 |     // If gin is listening a unix socket, always trust it.
     997 |     localAddr, ok := c.Request.Context().Value(http.LocalAddrContextKey).(net.Addr)
     998 |     if ok && strings.HasPrefix(localAddr.Network(), "unix") {
     999 |         trusted = true
    1000 |     }
    1001 | 
    1002 |     // Fallback
    1003 |     if !trusted {
    1004 |         // It also checks if the remoteIP is a trusted proxy or not.
    1005 |         // In order to perform this validation, it will see if the IP is contained within at least one of the CIDR blocks
    1006 |         // defined by Engine.SetTrustedProxies()
    1007 |         remoteIP = net.ParseIP(c.RemoteIP())
    1008 |         if remoteIP == nil {
    1009 |             return ""
    1010 |         }
    1011 |         trusted = c.engine.isTrustedProxy(remoteIP)
    1012 |     }
    1013 | 
    1014 |     if trusted && c.engine.ForwardedByClientIP && c.engine.RemoteIPHeaders != nil {
    1015 |         for _, headerName := range c.engine.RemoteIPHeaders {
    1016 |             headerValue := strings.Join(c.Request.Header.Values(headerName), ",")
    1017 |             ip, valid := c.engine.validateHeader(headerValue)
    1018 |             if valid {
    1019 |                 return ip
    1020 |             }
    1021 |         }
    1022 |     }
    1023 |     return remoteIP.String()
    1024 | }
  gin.go 的 validateHeader(Gin v1.12.0):
     482 | func (engine *Engine) validateHeader(header string) (clientIP string, valid bool) {
     483 |     if header == "" {
     484 |         return "", false
     485 |     }
     486 |     items := strings.Split(header, ",")
     487 |     for i := len(items) - 1; i >= 0; i-- {
     488 |         ipStr := strings.TrimSpace(items[i])
     489 |         ip := net.ParseIP(ipStr)
     490 |         if ip == nil {
     491 |             break
     492 |         }
     493 | 
     494 |         // X-Forwarded-For is appended by proxy
     495 |         // Check IPs in reverse order and stop when find untrusted proxy
     496 |         if (i == 0) || (!engine.isTrustedProxy(ip)) {
     497 |             return ipStr, true
     498 |         }
     499 |     }
     500 |     return "", false
     501 | }

ClientIP 先看平台头(TrustedPlatform),再看是不是 unix socket,再判断远端是不是受信任代理,只有受信任才会去解析转发头;转发头从右往左处理。这个顺序决定了后面每一个结果。

默认是“信任所有”

实测 · 本机执行输出
    默认值:RemoteIPHeaders = [X-Forwarded-For X-Real-IP]  ForwardedByClientIP = true
    直连,没有头                             RemoteAddr=203.0.113.9:5000     → ClientIP=203.0.113.9
    直连,自己伪造 XFF: 1.2.3.4               RemoteAddr=203.0.113.9:5000     → ClientIP=1.2.3.4
    直连,XFF: 1.1.1.1, 2.2.2.2           RemoteAddr=203.0.113.9:5000     → ClientIP=1.1.1.1
    直连,只有 X-Real-IP: 5.5.5.5           RemoteAddr=203.0.113.9:5000     → ClientIP=5.5.5.5
    XFF 与 X-Real-IP 都有                 RemoteAddr=203.0.113.9:5000     → ClientIP=1.1.1.1
    XFF 不是 IP(unknown)+ X-Real-IP      RemoteAddr=203.0.113.9:5000     → ClientIP=5.5.5.5
    XFF 带端口 1.2.3.4:5555               RemoteAddr=203.0.113.9:5000     → ClientIP=203.0.113.9
    两行 XFF 头                           RemoteAddr=203.0.113.9:5000     → ClientIP=1.1.1.1
    IPv6 远端                            RemoteAddr=[2001:db8::1]:5000   → ClientIP=2001:db8::1

gin.New() 默认的受信任代理是 0.0.0.0/0 与 ::/0,所以任何远端的 X-Forwarded-For、X-Real-IP 都被采信:不经过任何代理的直连客户端,自己写什么 IP 就是什么。debug 模式启动时会打印 “You trusted all proxies, this is NOT safe” 的警告,就是为了这个。

配置受信任代理

实测 · 本机执行输出
    攻击者直连并伪造 XFF                                   RemoteAddr=198.51.100.1:5000    → ClientIP=198.51.100.1
    经受信任代理 10.0.0.5,代理追加了真实客户端                     RemoteAddr=10.0.0.5:5000        → ClientIP=203.0.113.7
    攻击者伪造 + 代理追加:9.9.9.9, 203.0.113.7              RemoteAddr=10.0.0.5:5000        → ClientIP=203.0.113.7
    多层代理:203.0.113.7, 10.0.0.9, 10.0.0.8           RemoteAddr=10.0.0.5:5000        → ClientIP=203.0.113.7
    全是受信任地址:10.1.1.1, 10.2.2.2                     RemoteAddr=10.0.0.5:5000        → ClientIP=10.1.1.1
    受信任的单个地址 172.16.1.7                            RemoteAddr=172.16.1.7:80        → ClientIP=203.0.113.7
    172.16.1.8(同网段但没列出)                            RemoteAddr=172.16.1.8:80        → ClientIP=172.16.1.8
    受信任代理 + 非法的 XFF 末项                             RemoteAddr=10.0.0.5:5000        → ClientIP=10.0.0.5
    SetTrustedProxies 的错误:
      [10.0.0.0/33] → invalid CIDR address: 10.0.0.0/33
      [not-an-ip] → invalid IP address: not-an-ip
      [10.0.0.1 bad] → invalid IP address: bad
    SetTrustedProxies(nil)(不信任任何代理):10.0.0.5

生产环境应当 SetTrustedProxies 成你自己的反向代理地址(或 CIDR;单个 IP 会补成 /32 或 /128)。之后只有来自这些地址的请求才会被解析转发头,并且从 X-Forwarded-For 的右端开始、跳过受信任的代理,取第一个不受信任的地址——代理链会在右边追加,客户端伪造的内容在左边,被丢弃。如果根本没有代理,设成 nil。

头的选择

实测 · 本机执行输出
    RemoteIPHeaders=[X-Real-IP]                  → 5.5.5.5
    RemoteIPHeaders=[True-Client-IP, X-Forwarded-For] → 7.7.7.7
    ForwardedByClientIP=false                    → 10.0.0.5
    RemoteIPHeaders=nil                          → 10.0.0.5

RemoteIPHeaders 是按顺序检查的头名列表;某个头的值不是合法 IP(比如带端口、写了 unknown)就退到下一个。想完全不看头,把 ForwardedByClientIP 设为 false 或 RemoteIPHeaders 设为 nil。注意 XFF 里带端口的写法(1.2.3.4:5555)不被接受。

TrustedPlatform 的风险

实测 · 本机执行输出
    来自不受信任的地址 198.51.100.1,CF-Connecting-IP: 8.8.8.8 → 8.8.8.8
    CF-Connecting-IP: not-an-ip → "not-an-ip"
    CF-Connecting-IP: <script> → "<script>"
    没有这个头时回退到普通规则(受信任代理 + XFF)→ 203.0.113.7
    常量:Cloudflare="CF-Connecting-IP" GoogleAppEngine="X-Appengine-Remote-Addr" FlyIO="Fly-Client-IP"

设置了 TrustedPlatform(Cloudflare、Fly.io 等)后,只要那个头非空就直接返回,不检查请求是不是真的来自该平台,也不检查内容是不是 IP。如果源站可以被绕过平台直接访问,这个头就可以任意伪造;配合防火墙只放行平台的出口 IP 才安全。

分步演示

ClientIP 与受信任代理
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Go 1.27.1(brew)/ Gin v1.12.0(go.mod 里 require,源码在模块缓存里);用 go run / go test 直接编译运行,需要库时经代理从 proxy.golang.org 下载;HTTP 实验用 httptest 或本机回环端口(9700 以上);计时用 time.Now / testing.B,数字每次运行会有小幅抖动。请求用 httptest 构造并直接设置 req.RemoteAddr(没有经过真实的网络);所有 IP 取自文档保留地址段(203.0.113.0/24、198.51.100.0/24、2001:db8::/32)与私有地址;每种情形各一次
    • 没有验证:真实反向代理(nginx、Cloudflare)实际发送的头格式;unix socket 监听时的“一律信任”(源码里有,见后面 unix socket 的文章);IPv6 地址在 XFF 里的写法;c.RemoteIP 与 ClientIP 在 HTTP/2 下的取值;Gin 其他版本的默认信任范围(只测了 v1.12.0)
    京ICP备2026067502号-1京公网安备11010502063487号