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 = sanitize_ips(ips_from(@req.remote_addr)).last
# Could be a CSV list and/or repeated headers that were concatenated.
client_ips = sanitize_ips(ips_from(@req.client_ip)).reverse!
forwarded_ips = sanitize_ips(@req.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
header.strip.split(/[,\s]+/)
end
def sanitize_ips(ips)
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