webserver NGINX X-Forwarded-For / XFF / xforword

s

https://en.wikipedia.org/wiki/X-Forwarded-For

http://www.cnblogs.com/yihang/archive/2010/12/19/1910365.html

问题表象1:

拦截量从5点开始增多,从日志看是因为请求中x_forwarded_for字段为空,导致WAF获取不到用户的IP,WAF会将所有未获取到IP的请求当成同一个人发起,超过频率限制被拦截

分析问题1:

web访问软防火墙安全规则1:如果一个用户连续两次请求之前时间间隔不大于1秒,算1次;

web访问软防火墙安全规则2:B2C分区设置的每分钟一个用户此种情况拦截阈值为4200,除以集群系数,每台防火墙服务器上每分钟一个用户此种情况超过70次就会拦截。

解决问题1:

数据链路:用户 -- > CDN -- > F5 VIP(无配置x_forwarded_for导致) --> WAF(误认为同一IP,触发封禁规则) -->  B2C 主站。

解决方法1

数据链路:用户 -- > CDN -- > F5 VIP(无配置x_forwarded_for导致) --> WAF(同一F5 VIP设置白名单,临时解禁规则) -->  B2C 主站。

解决方法2

数据链路:用户 -- > CDN -- > F5 VIP(配置x_forwarded_for) --> WAF(不再认为同一VIP F5设备,终极解决) -->  B2C 主站。
先来看一下X-Forwarded-For的定义:
X-Forwarded-For:简称XFF头,它代表客户端,也就是HTTP的请求端真实的IP,只有在通过了HTTP 代理或者负载均衡服务器时才会添加该项。它不是RFC中定义的标准请求头信息,在squid缓存代理服务器开发文档中可以找到该项的详细介绍。
标准格式如下:
X-Forwarded-For: client1, proxy1, proxy2
从标准格式可以看出,X-Forwarded-For头信息可以有多个,中间用逗号分隔,第一项为真实的客户端ip,剩下的就是曾经经过的代理或负载均衡的ip地址,经过几个就会出现几个。
按照上图的Web架构图,可以很容易的看出,当用户请求经过CDN后到达Nginx负载均衡服务器时,其X-Forwarded-For头信息应该为 客户端IP,CDN的IP 但实际情况并非如此,
一般情况下CDN服务商为了自身安全考虑会将这个信息做些改动,只保留客户端IP。我们可以通过程序获得X-Forwarded-For信息或者通过Nginx的add header方法来设置返回头来查看。 下面来分析请求头到达Nginx负载均衡服务器的情况;在默认情况下,Nginx并不会对X-Forwarded-For头做任何的处理,除非用户使用proxy_set_header 参数设置: proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; $proxy_add_x_forwarded_for变量包含客户端请求头中的"X-Forwarded-For",与$remote_addr用逗号分开,如果没有"X-Forwarded-For" 请求头,则$proxy_add_x_forwarded_for等于$remote_addr。 $remote_addr变量的值是客户端的IP 当Nginx设置X-Forwarded-For等于$proxy_add_x_forwarded_for后会有两种情况发生 1、如果从CDN过来的请求没有设置X-Forwarded-For头(通常这种事情不会发生),而到了我们这里Nginx设置将其设置为$proxy_add_x_forwarded_for的话,X-Forwarded-For的信息应该为CDN的IP,
因为相对于Nginx负载均衡来说客户端即为CDN,这样的话,后端的web程序时死活也获得不了真实用户的IP的。 2、CDN设置了X-Forwarded-For,我们这里又设置了一次,且值为$proxy_add_x_forwarded_for的话,那么X-Forwarded-For的内容变成 ”客户端IP,Nginx负载均衡服务器IP“如果是这种情况的话,
那后端的程序通过X-Forwarded-For获得客户端IP,则取逗号分隔的第一项即可。 如上两点所说,如果我们知道了CDN设置了X-Forwarded-For信息,且只有客户端真实的IP的话,那么我们的Nginx负载均衡服务器可以不必理会该头,让它默认即可。 其实Nginx中还有一个$http_x_forwarded_for变量,这个变量中保存的内容就是请求中的X-Forwarded-For信息。如果后端获得X-Forwarded-For信息的程序兼容性不好的话(没有考虑到X-Forwarded-For含有多个IP的情况),
最好就不要将X-Forwarded-For设置为 $proxy_add_x_forwarded_for。应该设置为$http_x_forwarded_for或者干脆不设置!

Nginx 获取真实 IP 方案

https://www.cnblogs.com/snifferhu/p/5477810.html

问题根源:

基于七层的负载均衡系统,获取IP的原理都是通过XRI和XFF进行处理,从中选出“正常情况下”的源头IP,然而这两个Header都是普通的HTTP头,任何代理程序都可以轻易修改伪造它们,使得获取IP的逻辑失效。

解决依据:

TCP协议需要建立真实的网络链路,因此其信息可以认为是真实可靠难以伪造的。根据阿里SLB文档中获取真实IP的方法(https://help.aliyun.com/document_detail/slb/best-practice/get-real-ipaddress.html)得知,如果采用四层负载均衡,则SLB的后端系统可直接通过 remote address 获取到IP,如果采用七层负载均衡,后续系统需配合 http_realip_module 使用。

设置步骤:

1、在阿里SLB中将负载均衡模式设置为四层,并且打开获取真实IP的选项(默认打开);
2、在SLB后端的转发 nginx 中使用 $remote_addr 参数填写 XRI 和 XFF;
3、在应用中即可可通过 XRI 或 XFF 获取真实 IP;

试验环境:

客户端:Chrome + Postman
本地模拟:本地 nginx + 测试环境 nginx + 测试环境 app
四层负载均衡:阿里 SLB + 测试环境 nginx + 测试环境 app
七层负载均衡:阿里 SLB + release nginx + release app

试验步骤:

1、在测试环境 customer 应用中部署 test.jsp,内容为获取 request 信息;
2、双 nginx 模拟
2.1、在测试环境 nginx 中设置 XFF 规则如下:
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
2.2、在本地 nginx 中设置负载均衡到测试环境 nginx 地址,XFF 规则同上;
2.3、使用 Postman 直接发送请求:
可以发现XFF的第一个IP是正确的地址(由于本试验中本地 nginx 是系统的一部分,所以127.0.0.1算正确IP);
2.4、在请求中加入伪造的 XFF:
发现此时获取到的 XFF 已经被污染。
3、四层SLB测试
3.1、将测试环境 nginx 中的 XFF 规则设置为:
proxy_set_header X-Forwarded-For $remote_addr;
3.2、正常发送请求
发现获取了正确的 IP;
3.3、伪造 XFF 请求:
发现依然可以获取到正确的 IP;
4、七层SLB + http_realip_module 模块测试
4.1、将 release 环境 nginx 相关配置修改为(其中100.97.0.0/16为SLB所在网段):
set_real_ip_from 100.97.0.0/16;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
4.2、伪造XFF请求:
可以发现:HTTP头中多了一个 remoteip 的字段,其值始终为正确的客户 IP;同时,XFF 字段保留了所有代理链路信息(包括伪造的部分)。

试验总结:

1、无论哪种方案其实核心原理大同小异,都是利用四层TCP的连接信息获取实际IP参数,区别只在于:获取到的这个IP在何时、用何种方式传递到后面系统,以及后面系统如何接收该参数。
2、双 nginx 只是为了在可控环境模拟 HTTP IP 欺骗的原理;
3、四层 SLB 负载均衡方案思路是在整个系统入口(即SLB的四层处)覆盖掉原始的 XRI 或 XFF,在系统的后面部分便可充分信任这些参数;
4、七层 SLB 负载均衡方案思路是把系统入口处拿到的真实IP放在独立的 remoteip 参数中(当然如果需要也可在后续nginx中用该参数覆写 XRI 或 XFF,和上一个方案相同);

参考资料:

阿里云SLB获取真实IP的配置方法:
http_realip_module官方文档:
http_realip_module实现代码:
阿里SLB原理:
SLB官负载均衡配置:
 
 

测试用页面代码:

复制代码
<%@page contentType="text/html" pageEncoding="GBK"%>
<%@page import="java.util.*"%><!--使用Enumeration导入此包-->
<html>
<head>
    <title>接收全部请求参数的名称及对应的内容</title>
</head>
<body>
<%
    Enumeration enu=request.getHeaderNames();//取得全部头信息
    while(enu.hasMoreElements()){//以此取出头信息
        String headerName=(String)enu.nextElement();
        String headerValue=request.getHeader(headerName);//取出头信息内容
%>
        <h5><%=headerName%><font color="red">--></font>
        <font color="blue"><%=headerValue%></font></h5>
<%
    }
%>
</body>
</html>
复制代码
 



end

原文地址:https://www.cnblogs.com/lindows/p/9374861.html