日志和限流里的客户端 IP 可信吗?默认配置下,任何人加一个 X-Forwarded-For 头就能改写 c.ClientIP()。这篇用伪造的请求把 Gin 取客户端 IP 的规则逐项验证:默认值、SetTrustedProxies、多层代理、头的优先级,以及 TrustedPlatform 的风险。
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::1gin.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.5RemoteIPHeaders 是按顺序检查的头名列表;某个头的值不是合法 IP(比如带端口、写了 unknown)就退到下一个。想完全不看头,把 ForwardedByClientIP 设为 false 或 RemoteIPHeaders 设为 nil。注意 XFF 里带端口的写法(1.2.3.4:5555)不被接受。
来自不受信任的地址 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 才安全。