require 'ipaddr' module Geocoder module Request # The location() method is vulnerable to trivial IP spoofing. # Don't use it in authorization/authentication code, or any # other security-sensitive application. Use safe_location # instead. def location @location ||= Geocoder.search(geocoder_spoofable_ip, ip_address: true).first end # This safe_location() protects you from trivial IP spoofing. # For requests that go through a proxy that you haven't # whitelisted as trusted in your Rack config, you will get the # location for the IP of the last untrusted proxy in the chain, # not the original client IP. You WILL NOT get the location # corresponding to the original client IP for any request sent # through a non-whitelisted proxy. def safe_location @safe_location ||= Geocoder.search(ip, ip_address: true).first end # There's a whole zoo of nonstandard headers added by various # proxy softwares to indicate original client IP. # ANY of these can be trivially spoofed! # (except REMOTE_ADDR, which should by set by your server, # and is included at the end as a fallback. # Order does matter: we're following the convention established in # ActionDispatch::RemoteIp::GetIp::calculate_ip() # https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/middleware/remote_ip.rb # where the forwarded_for headers, possibly containing lists, # are arbitrarily preferred over headers expected to contain a # single address. GEOCODER_CANDIDATE_HEADERS = ['HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED', 'HTTP_X_CLIENT_IP', 'HTTP_CLIENT_IP', 'HTTP_X_REAL_IP', 'HTTP_X_CLUSTER_CLIENT_IP', 'REMOTE_ADDR'] def geocoder_spoofable_ip # We could use a more sophisticated IP-guessing algorithm here, # in which we'd try to resolve the use of different headers by # different proxies. The idea is that by comparing IPs repeated # in different headers, you can sometimes decide which header # was used by a proxy further along in the chain, and thus # prefer the headers used earlier. However, the gains might not # be worth the performance tradeoff, since this method is likely # to be called on every request in a lot of applications. GEOCODER_CANDIDATE_HEADERS.each do |header| if @env.has_key? header addrs = geocoder_split_ip_addresses(@env[header]) addrs = geocoder_remove_port_from_addresses(addrs) addrs = geocoder_reject_non_ipv4_addresses(addrs) addrs = geocoder_reject_trusted_ip_addresses(addrs) return addrs.first if addrs.any? end end @env['REMOTE_ADDR'] end private def geocoder_split_ip_addresses(ip_addresses) ip_addresses ? ip_addresses.strip.split(/[,\s]+/) : [] end # use Rack's trusted_proxy?() method to filter out IPs that have # been configured as trusted; includes private ranges by # default. (we don't want every lookup to return the location # of our own proxy/load balancer) def geocoder_reject_trusted_ip_addresses(ip_addresses) ip_addresses.reject { |ip| trusted_proxy?(ip) } end def geocoder_remove_port_from_addresses(ip_addresses) ip_addresses.map do |ip| # IPv4 if ip.count('.') > 0 ip.split(':').first # IPv6 bracket notation elsif match = ip.match(/\[(\S+)\]/) match.captures.first # IPv6 bare notation else ip end end end def geocoder_reject_non_ipv4_addresses(ip_addresses) ips = [] for ip in ip_addresses begin valid_ip = IPAddr.new(ip) rescue valid_ip = false end ips << valid_ip.to_s if valid_ip end return ips.any? ? ips : ip_addresses end end end ActionDispatch::Request.__send__(:include, Geocoder::Request) if defined?(ActionDispatch::Request) Rack::Request.__send__(:include, Geocoder::Request) if defined?(Rack::Request)