前言
服务通常会经过各种代理转发请求,但当我们要做请求身份判断、审计日志,以及一切与来源请求 IP 相关的功能时,要如何拿到正确的信息?这里记录一下我根据 Go Gin 可信代理 学习到的最佳安全实践。
请求 Header 可以被轻易伪造
在 Gin 中通常会使用 c.ClientIP() 方法来获取请求来源 IP,背后它会自动解析如 X-Forwarded-For 的请求 Header 来进行判断。
而通常通过反向代理的请求,会在 X-Forwarded-For 或 X-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 是不安全的,随便都能被伪造修改。
curl -s -H "X-Forwarded-For: 203.0.113.196" http://localhost:8080/ping解决方法一:不信任任何代理,使用 RemoteAddr
Gin 文档中提到 Engine.SetTrustedProxies(nil) 等同于不信任任何来源,直接使用 Request.RemoteAddr 作为结果(从 TCP 连接信息中获取),这样也能减少一些判断。
不过这种做法在没有使用请求 IP 的场景下还行,但一旦使用了代理服务器呢?所有请求来源都会变成自己的代理服务器。
解决方法二:正确设置受信任的代理
在请求存在代理的情况下,正确做法是明确告诉 Gin 哪些 IP 或 CIDR 范围是「可信任的代理」,这样 ClientIP() 只会在请求来自这些来源时才采信 X-Forwarded-For 请求头。
我认为「可信任的代理」这个名称有很大的误导性,会让人以为安全性的前提是提前声明了可信任的 IP 区间。
但更准确地说,它的运作原理更像是「忽略列表」,作用是跳过自己的代理服务。安全性的核心其实建立在「完全不相信 Header 中的数据」之上。
可以参考以下 Gin 获取 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 获取既安全又合理。之前文章中的错误内容,也随着理解加深而修正了。