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