1. 程式人生 > 實用技巧 >利用X-Forwarded-For偽造客戶端IP漏洞成因及防範

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

問題背景

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

如何獲取客戶端IP

在Java中,獲取客戶端IP最直接的方式就是使用request.getRemoteAddr()。這種方式能獲取到連線伺服器的客戶端IP,在中間沒有代理的情況下,的確是最簡單有效的方式。但是目前網際網路Web應用很少會將應用伺服器直接對外提供服務,一般都會有一層Nginx做反向代理和負載均衡,有的甚至可能有多層代理。在有反向代理的情況下,直接使用request.getRemoteAddr()獲取到的IP地址是Nginx所在伺服器的IP地址,而不是客戶端的IP。

HTTP協議是基於TCP協議的,由於request.getRemoteAddr()獲取到的是TCP層直接連線的客戶端的IP,對於Web應用伺服器來說直接連線它的客戶端實際上是Nginx,也就是TCP層是拿不到真實客戶端的IP。

為了解決上面的問題,很多HTTP代理會在HTTP協議頭中新增X-Forwarded-For頭,用來追蹤請求的來源。X-Forwarded-For的格式如下:


X-Forwarded-For: client1, proxy1, proxy2

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

右邊。

下面就是一種常用的獲取客戶端真實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頭,需要配置:


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;

這裡使用$remote_addr替代上面的$proxy_add_x_forwarded_for$proxy_add_x_forwarded_for會在原有X-Forwarded-For上追加IP,這就相當於給了偽造X-Forwarded-For的機會。而$remote_addr是獲取的是直接TCP連線的客戶端IP(類似於Java中的request.getRemoteAddr()),這個是無法偽造的,即使客戶端偽造也會被覆蓋掉,而不是追加。

需要注意的是,如果有多層代理,那麼只要在直接對外訪問的Nginx上配置X-Forwarded-For$remote_addr,內部層的Nginx還是要配置為$proxy_add_x_forwarded_for,不然內部層的Nginx又會覆蓋掉客戶端的真實IP。

方法二

另外一種方法是我在Tomcat原始碼中發現的:org.apache.catalina.valves.RemoteIpValve

實現思路:遍歷X-Forwarded-For頭中的IP地址,和上面方法不同的是,不是直接取左邊第一個IP,而是從右向左遍歷。遍歷時可以根據正則表示式剔除掉內網IP和已知的代理伺服器本身的IP(例如192.168開頭的),那麼拿到的第一個非剔除IP就會是一個可信任的客戶端IP。這種方法的巧妙之處在於,即時偽造X-Forwarded-For,那麼請求到達應用伺服器時,偽造的IP也會在X-Forwarded-For值的左邊,從右向左遍歷就可以避免取到這些偽造的IP地址。這種方式本文就不提供具體實現程式碼了,有興趣可以檢視Tomcat原始碼。