Gin IP retrieval security best practices

代理伺服器与后端服务如何安全处理请求的 IP 来源?

前言

服务通常会经过各种代理转发请求,但当我们要做请求身份判断、审计日志,以及一切与来源请求 IP 相关的功能时,要如何拿到正确的信息?这里记录一下我根据 Go Gin 可信代理🔗 学习到的最佳安全实践。

请求 Header 可以被轻易伪造

在 Gin 中通常会使用 c.ClientIP() 方法来获取请求来源 IP,背后它会自动解析如 X-Forwarded-For 的请求 Header 来进行判断。

而通常通过反向代理的请求,会在 X-Forwarded-ForX-Real-IP 等请求头中转发原始请求的 IP 地址,例如以下 Nginx 示例:

server {
listen 80;
server_name your-domain.com;
location / {
# 转发请求到 Gin 服务
proxy_pass http://127.0.0.1:8080;
# 传递真实访问 IP 到请求头
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; # 真实访问 IP
# 多个代理时,记录所有代理 IP(第一个为真实 IP)
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}

通常 X-Forwarded-For🔗 的惯例会像这样:

X-Forwarded-For: <client>, <proxy>, …, <proxyN>

让我惊讶的是,Gin 默认会信任所有代理,也就是说通常使用 ClientIP() 获取请求 IP 是不安全的,随便都能被伪造修改。

Terminal window
curl -s -H "X-Forwarded-For: 203.0.113.196" http://localhost:8080/ping

解决方法一:不信任任何代理,使用 RemoteAddr

Gin 文档中提到 Engine.SetTrustedProxies(nil) 等同于不信任任何来源,直接使用 Request.RemoteAddr 作为结果(从 TCP 连接信息中获取),这样也能减少一些判断。

不过这种做法在没有使用请求 IP 的场景下还行,但一旦使用了代理服务器呢?所有请求来源都会变成自己的代理服务器

Gin 服务器(SetTrustedProxies: nil)反向代理 (IP: 10.0.0.1)Gin 服务器(SetTrustedProxies: nil)反向代理 (IP: 10.0.0.1)判断:不信任任何 Header!直接取 RemoteAddr (TCP 来源)判断:不信任任何 Header!直接取 RemoteAddr (TCP 来源)访客 A (IP: 1.1.1.1)访客 B (IP: 2.2.2.2)发送请求 (IP: 1.1.1.1)1转发请求 (TCP 连接来源: 10.0.0.1)2返回响应 (Gin 判定此请求来自 10.0.0.1)3发送请求 (IP: 2.2.2.2)4转发请求 (TCP 连接来源: 10.0.0.1)5返回响应 (Gin 判定此请求依旧来自 10.0.0.1)6访客 A (IP: 1.1.1.1)访客 B (IP: 2.2.2.2)

解决方法二:正确设置受信任的代理

在请求存在代理的情况下,正确做法是明确告诉 Gin 哪些 IP 或 CIDR 范围是「可信任的代理」,这样 ClientIP() 只会在请求来自这些来源时才采信 X-Forwarded-For 请求头。

我认为「可信任的代理」这个名称有很大的误导性,会让人以为安全性的前提是提前声明了可信任的 IP 区间。

但更准确地说,它的运作原理更像是「忽略列表」,作用是跳过自己的代理服务。安全性的核心其实建立在「完全不相信 Header 中的数据」之上。

可以参考以下 Gin 获取 IP 的运作原理:

开始

TrustedPlatform 已设置?

直接读取该 Header

结束并返回

RemoteAddr 的 IP
在 trustedCIDRs 内?

返回 RemoteAddr 的 IP

解析 RemoteIPHeaders
例如 X-Forwarded-For

从右往左寻找
第一个不在信任列表内的 IP

返回该 IP

Nginx 负责将真实客户端 IP 写入请求头:

proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;

Gin 只信任来自 Nginx 所在网段的请求头:

r.SetTrustedProxies([]string{"172.29.0.0/16"})
r.GET("/ping", func(c *gin.Context) {
clientIP := c.ClientIP()
})

这样即使攻击者在请求中伪造 X-Forwarded-For,由于请求并不是来自“受信任的代理网段”,Gin 在计算并跳过所有代理后,最终会直接使用 TCP 连接的 RemoteAddr,而不是请求头中的内容,因此伪造也就失效了。

总结

与同事讨论后,让我重新反思并深入挖掘了所谓 ClientIP 背后 Gin 实际做了哪些策略,才能让 IP 获取既安全又合理。之前文章中的错误内容,也随着理解加深而修正了。

延伸阅读