X-Forwarded-For の正しい取り扱い方とCloudFrontを通したときのクライアントIPの取得方法
X-Forwarded-For ってなに?
プロキシやロードバランサーを通してサーバーにアクセスすると、サーバーから見たクライアントIPはプロキシのものになってしまいます。それを解決するために、プロキシで X-Forwarded-For
HTTP Header にクライアントの Remote IP を記録して、サーバーではその値を見ることでクライアントIPを取得できるようになります。
X-Forwarded-For からクライアントIPを取得する
X-Forwarded-For
HTTP Header は以下のようなフォーマットになっています。
X-Forwarded-For: <client>, <proxy1>, ...
そのためクライアントIPを得るためには X-Forwarded-For
の先頭のIPを参照すればよさそうです。
しかし、この方法には問題があります。というのもプロキシでは X-Forwarded-For
が存在していたのなら末尾に接続元のIPを追加するので、悪意のあるクライアントが X-Forwarded-For
に任意のIPを指定することによってアクセス元IPを偽装することが可能になります(IPスプーフィング)。たとえば、サーバー側で X-Forwarded-For
の値を元にIP制限をかけていた場合に突破される可能性があります。
X-Forwarded-For からクライアントIPをセキュアに取得する
ではどうするのでしょうか。たとえば Rails では信頼できるプロキシのIPを事前に登録しておき、X-Forwarded-For
のIPリストからその信頼できるプロキシのIPを除去し、末尾のIPをクライアントIPとして採用するというものがあります。
たとえば、サーバー到達時点の X-Forwarded-For
の値が以下のようなものであったとします。
X-Forwarded-For: <偽装されたIP> <クライアントの真のIP>, <プロキシ1のIP>, <プロキシ2のIP>
そして、信頼できるプロキシのIPリストには以下が登録されているとします。
信頼できるプロキシのIPリスト := [プロキシ1のIP, プロキシ2のIP]
このような条件下では次の動作をします。まず、X-Forwarded-For
から信頼できるプロキシのIPリストに登録しているIPを除去します。
X-Forwarded-For: <偽装されたIP> <クライアントの真のIP>
そして、その末尾のIPを取得します。これはクライアントの真のIPとなるので、偽装されたIPを取得することなく、悪意のあるクライアントからの攻撃を防げます。
Railsでの詳細
ActionDispatch::Request#remote_ip
では以下のようにして、信頼できるIPアドレスを X-Forwarded-For
から取得します。
module ActionDispatch class RemoteIp ... def call(env) req = ActionDispatch::Request.new env req.remote_ip = GetIp.new(req, check_ip, proxies) @app.call(req.env) end ... class GetIp ... def calculate_ip # Set by the Rack web server, this is a single value. remote_addr = ips_from(@req.remote_addr).last # Could be a CSV list and/or repeated headers that were concatenated. client_ips = ips_from(@req.client_ip).reverse forwarded_ips = ips_from(@req.x_forwarded_for).reverse # +Client-Ip+ and +X-Forwarded-For+ should not, generally, both be set. # If they are both set, it means that either: # # 1) This request passed through two proxies with incompatible IP header # conventions. # 2) The client passed one of +Client-Ip+ or +X-Forwarded-For+ # (whichever the proxy servers weren't using) themselves. # # Either way, there is no way for us to determine which header is the # right one after the fact. Since we have no idea, if we are concerned # about IP spoofing we need to give up and explode. (If you're not # concerned about IP spoofing you can turn the +ip_spoofing_check+ # option off.) should_check_ip = @check_ip && client_ips.last && forwarded_ips.last if should_check_ip && !forwarded_ips.include?(client_ips.last) # We don't know which came from the proxy, and which from the user raise IpSpoofAttackError, "IP spoofing attack?! " \ "HTTP_CLIENT_IP=#{@req.client_ip.inspect} " \ "HTTP_X_FORWARDED_FOR=#{@req.x_forwarded_for.inspect}" end # We assume these things about the IP headers: # # - X-Forwarded-For will be a list of IPs, one per proxy, or blank # - Client-Ip is propagated from the outermost proxy, or is blank # - REMOTE_ADDR will be the IP that made the request to Rack ips = [forwarded_ips, client_ips, remote_addr].flatten.compact # If every single IP option is in the trusted list, just return REMOTE_ADDR filter_proxies(ips).first || remote_addr end ... def filter_proxies(ips) # :doc: ips.reject do |ip| @proxies.any? { |proxy| proxy === ip } end end end ... end end
— rails/remote_ip.rb at c81af6ae723ccfcd601032167d7b7f57c5449c33 · rails/rails
そのため、config.action_dispatch.trusted_proxies
に前段にあるプロキシ/ロードバランサーのIPを設定します。
config/initializers/trusted_proxies.rb:
Rails.application.configure do # Proxy 1: 56.103.84.14 # Proxy 2: 56.103.84.15 config.action_dispatch.trusted_proxies = %w{ 56.103.84.14 56.103.84.15 }.map { |proxy| IPAddr.new(proxy) } end
このようにすることで、X-Forwarded-For
が以下のようになっていた場合にクライアントのIPが取得できます。
X-Forwarded-For: 103.10.21.3, 56.103.84.14 # Proxy 1、Proxy 2を経由した場合。103.22.34.5 がクライアントIP X-Forwarded-For: 17.20.1.3, 103.10.21.3, 56.103.84.14 # Proxy 1、Proxy 2を経由した場合。17.20.1.3 は偽装されたIP
class ApplicationController < ActionController::Base before_action :check_ip def check_ip if request.remote_ip == '17.20.1.3' ... end end end
CloudFrontを使用した場合に X-Forwarded-For からクライアントIPをセキュアに取得する
上記で述べた方法でクライアントIPをセキュアに取得できますが、 CloudFront - ALB - Server
という構成の場合は少々やっかいです。というのも CloudFront は世界中に数十ものエッジサーバーがあり、かつIPが変わり得るので、上記で述べたように CloudFront のIPを事前に信頼できるプロキシに登録するのは困難であるからです。
しかし、AWS はAWSの各種サービスの取り得るIPリストを取得できるAPI( https://ip-ranges.amazonaws.com/ip-ranges.json )が存在し、これをもとに信頼できるIPリストに登録できます。Rails で一番簡単なのは config/initializer などで、Rails起動時に https://ip-ranges.amazonaws.com/ip-ranges.json APIにアクセスし、信頼できるIPリストに登録するというものです。
config/initializers/trusted_proxies.rb:
Rails.application.configure do ip_ranges_res = Faraday.get('https://ip-ranges.amazonaws.com/ip-ranges.json') ip_ranges = JSON.parse(ip_ranges_res.body) cloudfront_ips = ip_ranges['prefixes'].select { |v| v['service'] == 'CLOUDFRONT' }.map { |v| IPAddr.new(v['ip_prefix']) } + ip_ranges['ipv6_prefixes'].select { |v| v['service'] == 'CLOUDFRONT' }.map { |v| IPAddr.new(v['ipv6_prefix']) } config.action_dispatch.trusted_proxies = cloudfront_ips end
まとめ
X-Forwarded-For
を使うことで、クライアント・サーバー間にプロキシやロードバランサが存在してもクライアントのIPを取得することができます。
しかし、適切に扱わないと脆弱性が発生するので注意する必要があります。