利用X-Forwarded-For僞造客戶端IP漏洞成因及防範

問題背景

在Web應用開發中,常常會須要獲取客戶端IP地址。一個典型的例子就是投票系統,爲了防止刷票,須要限制每一個IP地址只能投票一次。html

如何獲取客戶端IP

在Java中,獲取客戶端IP最直接的方式就是使用request.getRemoteAddr()。這種方式能獲取到鏈接服務器的客戶端IP,在中間沒有代理的狀況下,的確是最簡單有效的方式。可是目前互聯網Web應用不多會將應用服務器直接對外提供服務,通常都會有一層Nginx作反向代理和負載均衡,有的甚至可能有多層代理。在有反向代理的狀況下,直接使用request.getRemoteAddr()獲取到的IP地址是Nginx所在服務器的IP地址,而不是客戶端的IP。java

HTTP協議是基於TCP協議的,因爲request.getRemoteAddr()默認獲取到的是TCP層直接鏈接的客戶端的IP,對於Web應用服務器來講直接鏈接它的客戶端其實是Nginx,也就是TCP層是拿不到真實客戶端的IP。正則表達式

爲了解決上面的問題,不少HTTP代理會在HTTP協議頭中添加X-Forwarded-For頭,用來追蹤請求的來源。X-Forwarded-For的格式以下:apache

X-Forwarded-For: client1, proxy1, proxy2

X-Forwarded-For包含多個IP地址,每一個值經過逗號+空格分開,最左邊(client1)是最原始客戶端的IP地址,中間若是有多層代理,每一層代理會將鏈接它的客戶端IP追加在X-Forwarded-For右邊。api

下面就是一種經常使用的獲取客戶端真實IP的方法,首先從HTTP頭中獲取X-Forwarded-For,若是X-Forwarded-For頭存在就按逗號分隔取最左邊第一個IP地址,不存在直接經過request.getRemoteAddr()獲取IP地址:瀏覽器

public String getClientIp(HttpServletRequest request) {
    String xff = request.getHeader("X-Forwarded-For");
    if (xff == null) {
        return request.getRemoteAddr();
    } else {
        return xff.contains(",") ? xff.split(",")[0] : xff;
    }
}

另外,要讓Nginx支持X-Forwarded-For頭,須要配置:tomcat

proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

$proxy_add_x_forwarded_for會將和Nginx直接鏈接的客戶端IP追加在請求原有X-Forwarded-For值的右邊。服務器

僞造X-Forwarded-For

通常的客戶端(例如瀏覽器)發送HTTP請求是沒有X-Forwarded-For頭的,當請求到達第一個代理服務器時,代理服務器會加上X-Forwarded-For請求頭,並將值設爲客戶端的IP地址(也就是最左邊第一個值),後面若是還有多個代理,會依次將IP追加到X-Forwarded-For頭最右邊,最終請求到達Web應用服務器,應用經過獲取X-Forwarded-For頭取左邊第一個IP即爲客戶端真實IP。負載均衡

可是若是客戶端在發起請求時,請求頭上帶上一個僞造的X-Forwarded-For,因爲後續每層代理只會追加而不會覆蓋,那麼最終到達應用服務器時,獲取的左邊第一個IP地址將會是客戶端僞造的IP。也就是上面的Java代碼中getClientIp()方法獲取的IP地址頗有多是僞造的IP地址,若是一個投票系統用這種方式作的IP限制,那麼很容易會被刷票。框架

僞造X-Forwarded-For頭的方法很簡單,例如Postman就能夠輕鬆作到:
Postman僞造X-Forwarded-For

固然你也能夠寫一段刷票程序或者腳本,每次請求時添加X-Forwarded-For頭並隨機生成一個IP來實現刷票的目的。

如何防範

方法一

在直接對外的Nginx反向代理服務器上配置:

proxy_set_header X-Forwarded-For $remote_addr;

若是有多層Nginx代理,內層的Nginx配置:

proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

在最外層Nginx(即直接對外提供服務的Nginx)使用$remote_addr代替上面的$proxy_add_x_forwarded_for,能夠防止僞造X-Forwarded-For$proxy_add_x_forwarded_for會在原有X-Forwarded-For上追加IP,這就至關於給了僞造X-Forwarded-For的機會。而$remote_addr是獲取的是直接TCP鏈接的客戶端IP,這個是沒法僞造的,即便客戶端僞造也會被覆蓋掉,而不是追加。

須要注意的是,若是有多層代理,只在直接對外訪問的Nginx上配置X-Forwarded-For$remote_addr,內層的Nginx仍是要配置爲$proxy_add_x_forwarded_for,否則內層的Nginx又會覆蓋掉客戶端的真實IP。

完成以上配置後,業務代碼中再經過上面的getClientIp()方法,獲取X-Forwarded-For最左邊的IP地址即爲真實的客戶端地址,且客戶端也沒法僞造。

方法二

Tomcat服務器解決方案:org.apache.catalina.valves.RemoteIpValve

RemoteIpValve能夠替換Servlet API中request.getRemoteAddr()方法的實現,讓request.getRemoteAddr()方法從X-Forwarded-For頭中獲取IP地址。也就是在業務代碼中不須要再本身實現相似於上面的getClientIp()方法來從X-Forwarded-For中獲取IP,而是直接使用request.getRemoteAddr()方法。想要使用RemoteIpValve,僅須要在Tomcat配置文件server.xml中Host元素內末尾加上:

<Valve className="org.apache.catalina.valves.RemoteIpValve" ... />

RemoteIpValve有一套防止僞造X-Forwarded-For的機制,實現思路:遍歷X-Forwarded-For頭中的IP地址,和方法一不一樣的是,不是直接取左邊第一個IP,而是從右向左遍歷。遍歷時能夠根據正則表達式剔除掉內網IP和已知的代理服務器自己的IP(例如192.168開頭的IP),那麼拿到的第一個非剔除IP就會是一個可信任的客戶端IP。這種方法的巧妙之處在於,即便僞造X-Forwarded-For,那麼請求到達應用服務器時,僞造的IP也會在X-Forwarded-For值的左邊,真實的IP爲放到右邊的某個位置,從右向左遍歷就能夠避免取到這些僞造的IP地址。

方法三

Node.js 框架 Egg.js 的解決方案:https://eggjs.org/zh-cn/tutor...

Egg.js 可經過設置maxProxyCount指定代理層數,而後取X-Forwarded-For頭中從右往左數第maxProxyCount個IP即爲真實 IP 地址,若是有僞造 IP 地址了必然在最左邊,就會被忽略掉。

關注我

圖片描述

相關文章
相關標籤/搜索