前言
服務通常會經過各種代理轉發請求,但當我們要做判斷請求身份、審計日誌、一切與來源請求 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 獲取是安全合理的,前面文章的錯誤內容也隨著理解改正過。