new(app, ip_spoofing_check = true, custom_proxies = nil)
public Create a new RemoteIp middleware instance.
The ip_spoofing_check option is on by default. When on, an exception is raised if it looks like the client is trying to lie about its own IP address. It makes sense to turn off this check on sites aimed at non-IP clients (like WAP devices), or behind proxies that set headers in an incorrect or confusing way (like AWS ELB).
The custom_proxies argument can take an enumerable which will be used instead of TRUSTED_PROXIES. Any proxy setup will put the value you want in the middle (or at the beginning) of the X-Forwarded-For list, with your proxy servers after it. If your proxies aren’t removed, pass them in via the custom_proxies parameter. That way, the middleware will ignore those IP addresses, and return the one that you want.
Show source def initialize(app, ip_spoofing_check = true, custom_proxies = nil) @app = app @check_ip = ip_spoofing_check @proxies = if custom_proxies.blank? TRUSTED_PROXIES elsif custom_proxies.respond_to?(:any?) custom_proxies else raise(ArgumentError, <<~EOM) Setting config.action_dispatch.trusted_proxies to a single value isn't supported. Please set this to an enumerable instead. For example, instead of: config.action_dispatch.trusted_proxies = IPAddr.new("10.0.0.0/8") Wrap the value in an Array: config.action_dispatch.trusted_proxies = [IPAddr.new("10.0.0.0/8")] Note that passing an enumerable will *replace* the default set of trusted proxies. EOM end end # Since the IP address may not be needed, we store the object here # without calculating the IP to keep from slowing down the majority of # requests. For those requests that do need to know the IP, the # GetIp#calculate_ip method will calculate the memoized client IP address. def call(env) req = ActionDispatch::Request.new env req.remote_ip = GetIp.new(req, check_ip, proxies) @app.call(req.env) end # The GetIp class exists as a way to defer processing of the request data # into an actual IP address. If the ActionDispatch::Request#remote_ip method # is called, this class will calculate the value and then memoize it. class GetIp def initialize(req, check_ip, proxies) @req = req @check_ip = check_ip @proxies = proxies end # Sort through the various IP address headers, looking for the IP most # likely to be the address of the actual remote client making this # request. # # REMOTE_ADDR will be correct if the request is made directly against the # Ruby process, on e.g. Heroku. When the request is proxied by another # server like HAProxy or NGINX, the IP address that made the original # request will be put in an +X-Forwarded-For+ header. If there are multiple # proxies, that header may contain a list of IPs. Other proxy services # set the +Client-Ip+ header instead, so we check that too. # # As discussed in {this post about Rails IP Spoofing}[https://web.archive.org/web/20170626095448/https://blog.gingerlime.com/2012/rails-ip-spoofing-vulnerabilities-and-protection/], # while the first IP in the list is likely to be the "originating" IP, # it could also have been set by the client maliciously. # # In order to find the first address that is (probably) accurate, we # take the list of IPs, remove known and trusted proxies, and then take # the last address left, which was presumably set by one of those proxies. 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. should_check_ip = @check_ip && client_ips.last && forwarded_ips.last if should_check_ip && !forwarded_ips.include?(client_ips.last) raise IpSpoofAttackError, "IP spoofing attack?! " "HTTP_CLIENT_IP=#{@req.client_ip.inspect} " "HTTP_X_FORWARDED_FOR=#{@req.x_forwarded_for.inspect}" end ips = forwarded_ips + client_ips ips.compact! filter_proxies(ips + [remote_addr]).first || ips.last || remote_addr end def to_s @ip ||= calculate_ip end private def ips_from(header) return [] unless header ips = header.strip.split(/[,\s]+/) ips.select! do |ip| range = IPAddr.new(ip).to_range range.begin == range.end rescue ArgumentError nil end ips end def filter_proxies(ips) ips.reject do |ip| @proxies.any? { |proxy| proxy === ip } end end end