最近为开发的以太坊水龙头添加了根据IP地址进行限流的功能,在这过程中遇到并解决了X-Forwarded-For中的IP伪造的安全问题。
一般来说用户的IP可以通过与服务器建立TCP连接的客户端地址得到,如在Go中可以通过r.RemoteAddr
获取,但为了方便对HTTPS证书、域名、负载均衡等进行相关配置,应用会运行在Nginx反向代理后,在此情况下获取到的连接地址是代理服务器的IP而非用户的。
我们可以使用 X-Forwarded-For 请求头来解应用部署在反向代理后无法获取真实IP的问题,每次代理服务器转发请求到下一个服务器时,要把代理服务器的 IP 写入 X-Forwarded-For 中,这样在最末端的应用服务收到请求时,就会得到一个 IP 列表:
X-Forwarded-For: client, proxy1, proxy2
X-Forwarded-For 是一个 HTTP 扩展头部。HTTP/1.1(RFC 2616)协议并没有对它的定义,它最开始是由 Squid 这个缓存代理软件引入,用来表示 HTTP 请求端真实 IP。如今它已经成为事实上的标准,被各大 HTTP 代理、负载均衡等转发服务广泛使用,并被写入 RFC 7239(Forwarded HTTP Extension)标准之中。
水龙头应用要支持无论是否部署在反向代理后都可以获取到用户IP,通常的做法是首先从HTTP头中获取 X-Forwarded-For ,如果该头存在就按逗号分隔取最左边第一个IP地址,不存在直接通过r.RemoteAddr
获取IP地址,如realip包就是这样实现的:
https://github.com/tomasen/realip/blob/master/realip.go#L53-L84
但这种方式存在IP伪造的风险,即如果应用没有部署在反向代理后,而用户在发送HTTP请求时指定了虚假的X-Forwarded-For 头,这时虽然应用没有运行在反向代理后但仍从X-Forwarded-For 头获取了用户伪造的中的IP。
最初想到的解决方法是判断r.RemoteAddr
是否是内网地址,如果是再去解析 X-Forwarded-For 请求头来获取客户端的真实 IP,否则直接使用r.RemoteAddr
作为用户的真实IP。当然也可以加入配置参数来指定配置是否部署在反向代理后。
但问题真的解决了吗?
我们刚才提到每层反向代理会依次将请求IP追加到 X-Forwarded-For 头中,因此我们的第一层反向代理会将用户伪造的 X-Forwarded-For 当作来自上一层反向代理的结果,然后将用户的真实IP追加到伪造的X-Forwarded-For 后,最终形成如下序列,这样应用拿到的X-Forwarded-For 中第一个IP依然是用户伪造的数据。
X-Forwarded-For: illegalIp, client, proxy1, proxy2
最终我参考了Koa.js和Egg.js中的前置代理模式,即通过 maxIpsCount 来配置前置的反向代理数量,这样在获取请求真实IP地址时,就会忽略掉用户多传递的伪造IP地址了。 https://github.com/koajs/koa/blob/master/docs/api/request.md#requestips
|
|
使用Go实现的版本如下所示,与Koa.js中的区别是当反向代理数量不大于0时则使用r.RemoteAddr
中的IP,这样就支持了使用反向代理与否的两种场景,且避免了用户伪造X-Forwarded-For。
|
|
此外还了解到了另一个方案,即在第一层反向代理重置X-Forwarded-For头,但该水龙头应用需要适配HeroKu,Herou也是使用X-Forwarded-For获取用户IP,但其无法像Nginx那样重置HTTP header,因此上述配置反向代理的数量是一种更通用的方式。