How do proxy handle the IP source of requests?

代理伺服器與後端服務如何安全處理請求的 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 獲取是安全合理的,前面文章的錯誤內容也隨著理解改正過。

延伸閱讀