HTTP 請求頭中的 X-Forwarded-For

本文轉載自https://imququ.com/post/x-forwarded-for-header-in-http.htmlphp

 

我一直認爲,對於從事 Web 前端開發的同窗來講,HTTP 協議以及其餘常見的網絡知識屬於必備項。一方面,前端不少工做如 Web 性能優化,大部分規則都跟 HTTP、HTTPS、SPDY 和 TCP 等協議的特色直接對應,若是不從協議自己出發而是一味地照辦教條,極可能拔苗助長。另外一方面,隨着 Node.js 的發展壯大,愈來愈多的前端同窗開始寫服務端程序,甚至是服務端框架(ThinkJS 就是這樣由前端工程師開發,並有着衆多前端工程師用戶的 Node.js 框架),掌握必要的網絡知識,對於服務端程序安全、部署、運維等工做來講相當重要。html

個人博客有一個「HTTP 相關」專題,從此會陸續更新更多內容進去,歡迎關注。今天要說的是 HTTP 請求頭中的 X-Forwarded-For(XFF)。前端

背景

經過名字就知道,X-Forwarded-For 是一個 HTTP 擴展頭部。HTTP/1.1(RFC 2616)協議並無對它的定義,它最開始是由 Squid 這個緩存代理軟件引入,用來表示 HTTP 請求端真實 IP。現在它已經成爲事實上的標準,被各大 HTTP 代理、負載均衡等轉發服務普遍使用,並被寫入 RFC 7239(Forwarded HTTP Extension)標準之中。nginx

X-Forwarded-For 請求頭格式很是簡單,就這樣:shell

X-Forwarded-For: client, proxy1, proxy2

能夠看到,XFF 的內容由「英文逗號 + 空格」隔開的多個部分組成,最開始的是離服務端最遠的設備 IP,而後是每一級代理設備的 IP。緩存

若是一個 HTTP 請求到達服務器以前,通過了三個代理 Proxy一、Proxy二、Proxy3,IP 分別爲 IP一、IP二、IP3,用戶真實 IP 爲 IP0,那麼按照 XFF 標準,服務端最終會收到如下信息:安全

X-Forwarded-For: IP0, IP1, IP2

Proxy3 直連服務器,它會給 XFF 追加 IP2,表示它是在幫 Proxy2 轉發請求。列表中並無 IP3,IP3 能夠在服務端經過 Remote Address 字段得到。咱們知道 HTTP 鏈接基於 TCP 鏈接,HTTP 協議中沒有 IP 的概念,Remote Address 來自 TCP 鏈接,表示與服務端創建 TCP 鏈接的設備 IP,在這個例子裏就是 IP3。性能優化

Remote Address 沒法僞造,由於創建 TCP 鏈接須要三次握手,若是僞造了源 IP,沒法創建 TCP 鏈接,更不會有後面的 HTTP 請求。不一樣語言獲取 Remote Address 的方式不同,例如 php 是 $_SERVER["REMOTE_ADDR"],Node.js 是 req.connection.remoteAddress,但原理都同樣。服務器

問題

有了上面的背景知識,開始說問題。我用 Node.js 寫了一個最簡單的 Web Server 用於測試。HTTP 協議跟語言無關,這裏用 Node.js 只是爲了方便演示,換成任何其餘語言均可以獲得相同結論。另外本文用 Nginx 也是同樣的道理,若是有興趣,換成 Apache 或其餘 Web Server 也同樣。網絡

下面這段代碼會監聽 9009 端口,並在收到 HTTP 請求後,輸出一些信息:

JSvar http = require('http'); http.createServer(function (req, res) { res.writeHead(200, {'Content-Type': 'text/plain'}); res.write('remoteAddress: ' + req.connection.remoteAddress + '\n'); res.write('x-forwarded-for: ' + req.headers['x-forwarded-for'] + '\n'); res.write('x-real-ip: ' + req.headers['x-real-ip'] + '\n'); res.end(); }).listen(9009, '0.0.0.0'); 

這段代碼除了前面介紹過的 Remote Address 和 X-Forwarded-For,還有一個 X-Real-IP,這又是一個自定義頭部字段。X-Real-IP 一般被 HTTP 代理用來表示與它產生 TCP 鏈接的設備 IP,這個設備多是其餘代理,也多是真正的請求端。須要注意的是,X-Real-IP 目前並不屬於任何標準,代理和 Web 應用之間能夠約定用任何自定義頭來傳遞這個信息。

如今能夠用域名 + 端口號直接訪問這個 Node.js 服務,再配一個 Nginx 反向代理:

NGINXlocation / { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $http_host; proxy_set_header X-NginX-Proxy true; proxy_pass http://127.0.0.1:9009/; proxy_redirect off; } 

個人 Nginx 監聽 80 端口,因此不帶端口就能夠訪問 Nginx 轉發過的服務。

測試直接訪問 Node 服務:

BASHcurl http://t1.imququ.com:9009/ remoteAddress: 114.248.238.236 x-forwarded-for: undefined x-real-ip: undefined 

因爲個人電腦直接鏈接了 Node.js 服務,Remote Address 就是個人 IP。同時我並未指定額外的自定義頭,因此後兩個字段都是 undefined。

再來訪問 Nginx 轉發過的服務:

BASHcurl http://t1.imququ.com/

remoteAddress: 127.0.0.1 x-forwarded-for: 114.248.238.236 x-real-ip: 114.248.238.236 

這一次,個人電腦是經過 Nginx 訪問 Node.js 服務,獲得的 Remote Address 其實是 Nginx 的本地 IP。而前面 Nginx 配置中的這兩行起做用了,爲請求額外增長了兩個自定義頭:

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

實際上,在生產環境中部署 Web 應用,通常都採用上面第二種方式,有不少好處。但這就引入一個隱患:不少 Web 應用爲了獲取用戶真正的 IP,從 HTTP 請求頭中獲取 IP。

HTTP 請求頭能夠隨意構造,咱們經過 curl 的 -H 參數構造 X-Forwarded-For 和 X-Real-IP,再來測試一把。

直接訪問 Node.js 服務:

BASHcurl http://t1.imququ.com:9009/ -H 'X-Forwarded-For: 1.1.1.1' -H 'X-Real-IP: 2.2.2.2' remoteAddress: 114.248.238.236 x-forwarded-for: 1.1.1.1 x-real-ip: 2.2.2.2 

對於 Web 應用來講,X-Forwarded-For 和 X-Real-IP 就是兩個普通的請求頭,天然就不作任何處理原樣輸出了。這說明,對於直連部署方式,除了從 TCP 鏈接中獲得的 Remote Address 以外,請求頭中攜帶的 IP 信息都不能信。

訪問 Nginx 轉發過的服務:

BASHcurl http://t1.imququ.com/ -H 'X-Forwarded-For: 1.1.1.1' -H 'X-Real-IP: 2.2.2.2' remoteAddress: 127.0.0.1 x-forwarded-for: 1.1.1.1, 114.248.238.236 x-real-ip: 114.248.238.236 

這一次,Nginx 會在 X-Forwarded-For 後追加個人 IP;並用個人 IP 覆蓋 X-Real-IP 請求頭。這說明,有了 Nginx 的加工,X-Forwarded-For 最後一節以及 X-Real-IP 整個內容沒法構造,能夠用於獲取用戶 IP。

用戶 IP 每每會被使用與跟 Web 安全有關的場景上,例如檢查用戶登陸地區,基於 IP 作訪問頻率控制等等。這種場景下,確保 IP 沒法構造更重要。通過前面的測試和分析,對於直接面向用戶部署的 Web 應用,必須使用從 TCP 鏈接中獲得的 Remote Address;對於部署了 Nginx 這樣反向代理的 Web 應用,在正確配置了 Set Header 行爲後,可使用 Nginx 傳過來的 X-Real-IP 或 X-Forwarded-For 最後一節(實際上它們必定等價)。

那麼,Web 應用自身如何判斷請求是直接過來,仍是由可控的代理轉發來的呢?在代理轉發時增長額外的請求頭是一個辦法,可是不怎麼保險,由於請求頭太容易構造了。若是必定要這麼用,這個自定義頭要夠長夠罕見,還要保管好不能泄露出去。

判斷 Remote Address 是否是本地 IP 也是一種辦法,不過也不完善,由於在 Nginx 所處服務器上訪問,不管直連仍是走 Nginx 代理,Remote Address 都是 127.0.0.1。這個問題還好一般能夠忽略,更麻煩的是,反向代理服務器和實際的 Web 應用不必定部署在同一臺服務器上。因此更合理的作法是收集全部代理服務器 IP 列表,Web 應用拿到 Remote Address 後逐一比對來判斷是以何種方式訪問。

一般,爲了簡化邏輯,生產環境會封掉經過帶端口直接訪問 Web 應用的形式,只容許經過 Nginx 來訪問。那是否是這樣就沒問題了呢?也不見得。

首先,若是用戶真的是經過代理訪問 Nginx,X-Forwarded-For 最後一節以及 X-Real-IP 獲得的是代理的 IP,安全相關的場景只能用這個,但有些場景如根據 IP 顯示所在地天氣,就須要儘量得到用戶真實 IP,這時候 X-Forwarded-For 中第一個 IP 就能夠排上用場了。這時候須要注意一個問題,仍是拿以前的例子作測試:

BASHcurl http://t1.imququ.com/ -H 'X-Forwarded-For: unknown, <>"1.1.1.1' remoteAddress: 127.0.0.1 x-forwarded-for: unknown, <>"1.1.1.1, 114.248.238.236 x-real-ip: 114.248.238.236 

X-Forwarded-For 最後一節是 Nginx 追加上去的,但以前部分都來自於 Nginx 收到的請求頭,這部分用戶輸入內容徹底不可信。使用時須要格外當心,符合 IP 格式才能使用,否則容易引起 SQL 注入或 XSS 等安全漏洞。

結論

  1. 直接對外提供服務的 Web 應用,在進行與安全有關的操做時,只能經過 Remote Address 獲取 IP,不能相信任何請求頭;
  2. 使用 Nginx 等 Web Server 進行反向代理的 Web 應用,在配置正確的前提下,要用 X-Forwarded-For 最後一節 或 X-Real-IP 來獲取 IP(由於 Remote Address 獲得的是 Nginx 所在服務器的內網 IP);同時還應該禁止 Web 應用直接對外提供服務;
  3. 在與安全無關的場景,例如經過 IP 顯示所在地天氣,能夠從 X-Forwarded-For 靠前的位置獲取 IP,可是須要校驗 IP 格式合法性;

PS:網上有些文章建議這樣配置 Nginx,其實並不合理:

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

這樣配置以後,安全性確實提升了,可是也致使請求到達 Nginx 以前的全部代理信息都被抹掉,沒法爲真正使用代理的用戶提供更好的服務。仍是應該弄明白這中間的原理,具體場景具體分析。

本文連接:https://imququ.com/post/x-forwarded-for-header-in-http.html參與評論 »

--EOF--

相關文章
相關標籤/搜索