Merge pull request #5980 from gazay/valid_ips
Remote ip logic and validation. IPv6 support.
This commit is contained in:
commit
bb0906e2d7
@ -5,11 +5,14 @@ class IpSpoofAttackError < StandardError ; end
|
||||
# IP addresses that are "trusted proxies" that can be stripped from
|
||||
# the comma-delimited list in the X-Forwarded-For header. See also:
|
||||
# http://en.wikipedia.org/wiki/Private_network#Private_IPv4_address_spaces
|
||||
# http://en.wikipedia.org/wiki/Private_network#Private_IPv6_addresses.
|
||||
TRUSTED_PROXIES = %r{
|
||||
^127\.0\.0\.1$ | # localhost
|
||||
^::1$ |
|
||||
^(10 | # private IP 10.x.x.x
|
||||
172\.(1[6-9]|2[0-9]|3[0-1]) | # private IP in the range 172.16.0.0 .. 172.31.255.255
|
||||
192\.168 # private IP 192.168.x.x
|
||||
192\.168 | # private IP 192.168.x.x
|
||||
fc00:: # private IP fc00
|
||||
)\.
|
||||
}x
|
||||
|
||||
@ -19,13 +22,13 @@ def initialize(app, check_ip_spoofing = true, custom_proxies = nil)
|
||||
@app = app
|
||||
@check_ip = check_ip_spoofing
|
||||
@proxies = case custom_proxies
|
||||
when Regexp
|
||||
custom_proxies
|
||||
when nil
|
||||
TRUSTED_PROXIES
|
||||
else
|
||||
Regexp.union(TRUSTED_PROXIES, custom_proxies)
|
||||
end
|
||||
when Regexp
|
||||
custom_proxies
|
||||
when nil
|
||||
TRUSTED_PROXIES
|
||||
else
|
||||
Regexp.union(TRUSTED_PROXIES, custom_proxies)
|
||||
end
|
||||
end
|
||||
|
||||
def call(env)
|
||||
@ -34,6 +37,31 @@ def call(env)
|
||||
end
|
||||
|
||||
class GetIp
|
||||
|
||||
# IP v4 and v6 (with compression) validation regexp
|
||||
# https://gist.github.com/1289635
|
||||
VALID_IP = %r{
|
||||
(^(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[0-9]{1,2})(\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[0-9]{1,2})){3}$) | # ip v4
|
||||
(^(
|
||||
(([0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}) | # ip v6 not abbreviated
|
||||
(([0-9A-Fa-f]{1,4}:){6}:[0-9A-Fa-f]{1,4}) | # ip v6 with double colon in the end
|
||||
(([0-9A-Fa-f]{1,4}:){5}:([0-9A-Fa-f]{1,4}:)?[0-9A-Fa-f]{1,4}) | # - ip addresses v6
|
||||
(([0-9A-Fa-f]{1,4}:){4}:([0-9A-Fa-f]{1,4}:){0,2}[0-9A-Fa-f]{1,4}) | # - with
|
||||
(([0-9A-Fa-f]{1,4}:){3}:([0-9A-Fa-f]{1,4}:){0,3}[0-9A-Fa-f]{1,4}) | # - double colon
|
||||
(([0-9A-Fa-f]{1,4}:){2}:([0-9A-Fa-f]{1,4}:){0,4}[0-9A-Fa-f]{1,4}) | # - in the middle
|
||||
(([0-9A-Fa-f]{1,4}:){6} ((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3} (\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)) | # ip v6 with compatible to v4
|
||||
(([0-9A-Fa-f]{1,4}:){1,5}:((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)) | # ip v6 with compatible to v4
|
||||
(([0-9A-Fa-f]{1,4}:){1}:([0-9A-Fa-f]{1,4}:){0,4}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)) | # ip v6 with compatible to v4
|
||||
(([0-9A-Fa-f]{1,4}:){0,2}:([0-9A-Fa-f]{1,4}:){0,3}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)) | # ip v6 with compatible to v4
|
||||
(([0-9A-Fa-f]{1,4}:){0,3}:([0-9A-Fa-f]{1,4}:){0,2}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)) | # ip v6 with compatible to v4
|
||||
(([0-9A-Fa-f]{1,4}:){0,4}:([0-9A-Fa-f]{1,4}:){1}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)) | # ip v6 with compatible to v4
|
||||
(::([0-9A-Fa-f]{1,4}:){0,5}((\b((25[0-5])|(1\d{2})|(2[0-4]\d) |(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)) | # ip v6 with compatible to v4
|
||||
([0-9A-Fa-f]{1,4}::([0-9A-Fa-f]{1,4}:){0,5}[0-9A-Fa-f]{1,4}) | # ip v6 with compatible to v4
|
||||
(::([0-9A-Fa-f]{1,4}:){0,6}[0-9A-Fa-f]{1,4}) | # ip v6 with double colon at the begining
|
||||
(([0-9A-Fa-f]{1,4}:){1,7}:) # ip v6 without ending
|
||||
)$)
|
||||
}x
|
||||
|
||||
def initialize(env, middleware)
|
||||
@env = env
|
||||
@middleware = middleware
|
||||
@ -44,25 +72,31 @@ def initialize(env, middleware)
|
||||
# but will be wrong if the user is behind a proxy. Proxies will set
|
||||
# HTTP_CLIENT_IP and/or HTTP_X_FORWARDED_FOR, so we prioritize those.
|
||||
# HTTP_X_FORWARDED_FOR may be a comma-delimited list in the case of
|
||||
# multiple chained proxies. The last address which is not a known proxy
|
||||
# will be the originating IP.
|
||||
# multiple chained proxies. The first address which is in this list
|
||||
# if it's not a known proxy will be the originating IP.
|
||||
# Format of HTTP_X_FORWARDED_FOR:
|
||||
# client_ip, proxy_ip1, proxy_ip2...
|
||||
# http://en.wikipedia.org/wiki/X-Forwarded-For
|
||||
def calculate_ip
|
||||
client_ip = @env['HTTP_CLIENT_IP']
|
||||
forwarded_ips = ips_from('HTTP_X_FORWARDED_FOR')
|
||||
remote_addrs = ips_from('REMOTE_ADDR')
|
||||
client_ip = @env['HTTP_CLIENT_IP']
|
||||
forwarded_ip = ips_from('HTTP_X_FORWARDED_FOR').first
|
||||
remote_addrs = ips_from('REMOTE_ADDR')
|
||||
|
||||
check_ip = client_ip && @middleware.check_ip
|
||||
if check_ip && !forwarded_ips.include?(client_ip)
|
||||
if check_ip && forwarded_ip != client_ip
|
||||
# We don't know which came from the proxy, and which from the user
|
||||
raise IpSpoofAttackError, "IP spoofing attack?!" \
|
||||
"HTTP_CLIENT_IP=#{@env['HTTP_CLIENT_IP'].inspect}" \
|
||||
"HTTP_X_FORWARDED_FOR=#{@env['HTTP_X_FORWARDED_FOR'].inspect}"
|
||||
end
|
||||
|
||||
not_proxy = client_ip || forwarded_ips.first || remote_addrs.first
|
||||
|
||||
# Return first REMOTE_ADDR if there are no other options
|
||||
not_proxy || ips_from('REMOTE_ADDR', :allow_proxies).first
|
||||
client_ips = remove_proxies [client_ip, forwarded_ip, remote_addrs].flatten
|
||||
if client_ips.present?
|
||||
client_ips.first
|
||||
else
|
||||
# If there is no client ip we can return first valid proxy ip from REMOTE_ADDR
|
||||
remote_addrs.find { |ip| valid_ip? ip }
|
||||
end
|
||||
end
|
||||
|
||||
def to_s
|
||||
@ -71,12 +105,24 @@ def to_s
|
||||
@ip = calculate_ip
|
||||
end
|
||||
|
||||
protected
|
||||
private
|
||||
|
||||
def ips_from(header, allow_proxies = false)
|
||||
ips = @env[header] ? @env[header].strip.split(/[,\s]+/) : []
|
||||
allow_proxies ? ips : ips.reject{|ip| ip =~ @middleware.proxies }
|
||||
def ips_from(header)
|
||||
@env[header] ? @env[header].strip.split(/[,\s]+/) : []
|
||||
end
|
||||
|
||||
def valid_ip?(ip)
|
||||
ip =~ VALID_IP
|
||||
end
|
||||
|
||||
def not_a_proxy?(ip)
|
||||
ip !~ @middleware.proxies
|
||||
end
|
||||
|
||||
def remove_proxies(ips)
|
||||
ips.select { |ip| valid_ip?(ip) && not_a_proxy?(ip) }
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
@ -35,37 +35,40 @@ def url_for(options = {})
|
||||
assert_equal '1.2.3.4', request.remote_ip
|
||||
|
||||
request = stub_request 'REMOTE_ADDR' => '1.2.3.4',
|
||||
'HTTP_X_FORWARDED_FOR' => '3.4.5.6'
|
||||
'HTTP_X_FORWARDED_FOR' => '3.4.5.6'
|
||||
assert_equal '3.4.5.6', request.remote_ip
|
||||
|
||||
request = stub_request 'REMOTE_ADDR' => '127.0.0.1',
|
||||
'HTTP_X_FORWARDED_FOR' => '3.4.5.6'
|
||||
'HTTP_X_FORWARDED_FOR' => '3.4.5.6'
|
||||
assert_equal '3.4.5.6', request.remote_ip
|
||||
|
||||
request = stub_request 'HTTP_X_FORWARDED_FOR' => '3.4.5.6,unknown'
|
||||
assert_equal '3.4.5.6', request.remote_ip
|
||||
|
||||
request = stub_request 'HTTP_X_FORWARDED_FOR' => '172.16.0.1,3.4.5.6'
|
||||
assert_equal '3.4.5.6', request.remote_ip
|
||||
assert_equal nil, request.remote_ip
|
||||
|
||||
request = stub_request 'HTTP_X_FORWARDED_FOR' => '192.168.0.1,3.4.5.6'
|
||||
assert_equal '3.4.5.6', request.remote_ip
|
||||
assert_equal nil, request.remote_ip
|
||||
|
||||
request = stub_request 'HTTP_X_FORWARDED_FOR' => '10.0.0.1,3.4.5.6'
|
||||
assert_equal '3.4.5.6', request.remote_ip
|
||||
assert_equal nil, request.remote_ip
|
||||
|
||||
request = stub_request 'HTTP_X_FORWARDED_FOR' => '10.0.0.1, 10.0.0.1, 3.4.5.6'
|
||||
assert_equal '3.4.5.6', request.remote_ip
|
||||
assert_equal nil, request.remote_ip
|
||||
|
||||
request = stub_request 'HTTP_X_FORWARDED_FOR' => '127.0.0.1,3.4.5.6'
|
||||
assert_equal '3.4.5.6', request.remote_ip
|
||||
assert_equal nil, request.remote_ip
|
||||
|
||||
request = stub_request 'HTTP_X_FORWARDED_FOR' => 'unknown,192.168.0.1'
|
||||
assert_equal 'unknown', request.remote_ip
|
||||
assert_equal nil, request.remote_ip
|
||||
|
||||
request = stub_request 'HTTP_X_FORWARDED_FOR' => '3.4.5.6, 9.9.9.9, 10.0.0.1, 172.31.4.4'
|
||||
assert_equal '3.4.5.6', request.remote_ip
|
||||
|
||||
request = stub_request 'HTTP_X_FORWARDED_FOR' => 'not_ip_address'
|
||||
assert_equal nil, request.remote_ip
|
||||
|
||||
request = stub_request 'HTTP_X_FORWARDED_FOR' => '1.1.1.1',
|
||||
'HTTP_CLIENT_IP' => '2.2.2.2'
|
||||
e = assert_raise(ActionDispatch::RemoteIp::IpSpoofAttackError) {
|
||||
@ -89,6 +92,68 @@ def url_for(options = {})
|
||||
assert_equal '9.9.9.9', request.remote_ip
|
||||
end
|
||||
|
||||
test "remote ip v6" do
|
||||
request = stub_request 'REMOTE_ADDR' => '2001:0db8:85a3:0000:0000:8a2e:0370:7334'
|
||||
assert_equal '2001:0db8:85a3:0000:0000:8a2e:0370:7334', request.remote_ip
|
||||
|
||||
request = stub_request 'REMOTE_ADDR' => '2001:0db8:85a3:0000:0000:8a2e:0370:7334,fe80:0000:0000:0000:0202:b3ff:fe1e:8329'
|
||||
assert_equal '2001:0db8:85a3:0000:0000:8a2e:0370:7334', request.remote_ip
|
||||
|
||||
request = stub_request 'REMOTE_ADDR' => '2001:0db8:85a3:0000:0000:8a2e:0370:7334',
|
||||
'HTTP_X_FORWARDED_FOR' => 'fe80:0000:0000:0000:0202:b3ff:fe1e:8329'
|
||||
assert_equal 'fe80:0000:0000:0000:0202:b3ff:fe1e:8329', request.remote_ip
|
||||
|
||||
request = stub_request 'REMOTE_ADDR' => '::1',
|
||||
'HTTP_X_FORWARDED_FOR' => 'fe80:0000:0000:0000:0202:b3ff:fe1e:8329'
|
||||
assert_equal 'fe80:0000:0000:0000:0202:b3ff:fe1e:8329', request.remote_ip
|
||||
|
||||
request = stub_request 'HTTP_X_FORWARDED_FOR' => 'unknown,fe80:0000:0000:0000:0202:b3ff:fe1e:8329'
|
||||
assert_equal nil, request.remote_ip
|
||||
|
||||
request = stub_request 'HTTP_X_FORWARDED_FOR' => '::1,fe80:0000:0000:0000:0202:b3ff:fe1e:8329'
|
||||
assert_equal nil, request.remote_ip
|
||||
|
||||
request = stub_request 'HTTP_X_FORWARDED_FOR' => '::1,fe80:0000:0000:0000:0202:b3ff:fe1e:8329'
|
||||
assert_equal nil, request.remote_ip
|
||||
|
||||
request = stub_request 'HTTP_X_FORWARDED_FOR' => '::1,fe80:0000:0000:0000:0202:b3ff:fe1e:8329'
|
||||
assert_equal nil, request.remote_ip
|
||||
|
||||
request = stub_request 'HTTP_X_FORWARDED_FOR' => '::1, ::1, fe80:0000:0000:0000:0202:b3ff:fe1e:8329'
|
||||
assert_equal nil, request.remote_ip
|
||||
|
||||
request = stub_request 'HTTP_X_FORWARDED_FOR' => 'unknown,::1'
|
||||
assert_equal nil, request.remote_ip
|
||||
|
||||
request = stub_request 'HTTP_X_FORWARDED_FOR' => '2001:0db8:85a3:0000:0000:8a2e:0370:7334, fe80:0000:0000:0000:0202:b3ff:fe1e:8329, ::1, fc00::'
|
||||
assert_equal '2001:0db8:85a3:0000:0000:8a2e:0370:7334', request.remote_ip
|
||||
|
||||
request = stub_request 'HTTP_X_FORWARDED_FOR' => 'not_ip_address'
|
||||
assert_equal nil, request.remote_ip
|
||||
|
||||
request = stub_request 'HTTP_X_FORWARDED_FOR' => 'fe80:0000:0000:0000:0202:b3ff:fe1e:8329',
|
||||
'HTTP_CLIENT_IP' => '2001:0db8:85a3:0000:0000:8a2e:0370:7334'
|
||||
e = assert_raise(ActionDispatch::RemoteIp::IpSpoofAttackError) {
|
||||
request.remote_ip
|
||||
}
|
||||
assert_match(/IP spoofing attack/, e.message)
|
||||
assert_match(/HTTP_X_FORWARDED_FOR="fe80:0000:0000:0000:0202:b3ff:fe1e:8329"/, e.message)
|
||||
assert_match(/HTTP_CLIENT_IP="2001:0db8:85a3:0000:0000:8a2e:0370:7334"/, e.message)
|
||||
|
||||
# Turn IP Spoofing detection off.
|
||||
# This is useful for sites that are aimed at non-IP clients. The typical
|
||||
# example is WAP. Since the cellular network is not IP based, it's a
|
||||
# leap of faith to assume that their proxies are ever going to set the
|
||||
# HTTP_CLIENT_IP/HTTP_X_FORWARDED_FOR headers properly.
|
||||
request = stub_request 'HTTP_X_FORWARDED_FOR' => 'fe80:0000:0000:0000:0202:b3ff:fe1e:8329',
|
||||
'HTTP_CLIENT_IP' => '2001:0db8:85a3:0000:0000:8a2e:0370:7334',
|
||||
:ip_spoofing_check => false
|
||||
assert_equal '2001:0db8:85a3:0000:0000:8a2e:0370:7334', request.remote_ip
|
||||
|
||||
request = stub_request 'HTTP_X_FORWARDED_FOR' => 'fe80:0000:0000:0000:0202:b3ff:fe1e:8329, 2001:0db8:85a3:0000:0000:8a2e:0370:7334'
|
||||
assert_equal 'fe80:0000:0000:0000:0202:b3ff:fe1e:8329', request.remote_ip
|
||||
end
|
||||
|
||||
test "remote ip when the remote ip middleware returns nil" do
|
||||
request = stub_request 'REMOTE_ADDR' => '127.0.0.1'
|
||||
assert_equal '127.0.0.1', request.remote_ip
|
||||
@ -97,29 +162,47 @@ def url_for(options = {})
|
||||
test "remote ip with user specified trusted proxies String" do
|
||||
@trusted_proxies = "67.205.106.73"
|
||||
|
||||
request = stub_request 'REMOTE_ADDR' => '67.205.106.73',
|
||||
'HTTP_X_FORWARDED_FOR' => '3.4.5.6'
|
||||
request = stub_request 'REMOTE_ADDR' => '3.4.5.6',
|
||||
'HTTP_X_FORWARDED_FOR' => '67.205.106.73'
|
||||
assert_equal '3.4.5.6', request.remote_ip
|
||||
|
||||
request = stub_request 'REMOTE_ADDR' => '172.16.0.1,67.205.106.73',
|
||||
'HTTP_X_FORWARDED_FOR' => '3.4.5.6'
|
||||
assert_equal '3.4.5.6', request.remote_ip
|
||||
'HTTP_X_FORWARDED_FOR' => '67.205.106.73'
|
||||
assert_equal '172.16.0.1', request.remote_ip
|
||||
|
||||
request = stub_request 'REMOTE_ADDR' => '67.205.106.73,172.16.0.1',
|
||||
'HTTP_X_FORWARDED_FOR' => '3.4.5.6'
|
||||
assert_equal '3.4.5.6', request.remote_ip
|
||||
|
||||
request = stub_request 'REMOTE_ADDR' => '67.205.106.74,172.16.0.1',
|
||||
'HTTP_X_FORWARDED_FOR' => '3.4.5.6'
|
||||
request = stub_request 'REMOTE_ADDR' => '67.205.106.73,3.4.5.6',
|
||||
'HTTP_X_FORWARDED_FOR' => '67.205.106.73'
|
||||
assert_equal '3.4.5.6', request.remote_ip
|
||||
|
||||
request = stub_request 'HTTP_X_FORWARDED_FOR' => 'unknown,67.205.106.73'
|
||||
assert_equal 'unknown', request.remote_ip
|
||||
assert_equal nil, request.remote_ip
|
||||
|
||||
request = stub_request 'HTTP_X_FORWARDED_FOR' => '3.4.5.6, 9.9.9.9, 10.0.0.1, 67.205.106.73'
|
||||
assert_equal '3.4.5.6', request.remote_ip
|
||||
end
|
||||
|
||||
test "remote ip v6 with user specified trusted proxies String" do
|
||||
@trusted_proxies = 'fe80:0000:0000:0000:0202:b3ff:fe1e:8329'
|
||||
|
||||
request = stub_request 'REMOTE_ADDR' => '2001:0db8:85a3:0000:0000:8a2e:0370:7334',
|
||||
'HTTP_X_FORWARDED_FOR' => 'fe80:0000:0000:0000:0202:b3ff:fe1e:8329'
|
||||
assert_equal '2001:0db8:85a3:0000:0000:8a2e:0370:7334', request.remote_ip
|
||||
|
||||
request = stub_request 'REMOTE_ADDR' => 'fe80:0000:0000:0000:0202:b3ff:fe1e:8329,2001:0db8:85a3:0000:0000:8a2e:0370:7334',
|
||||
'HTTP_X_FORWARDED_FOR' => 'fe80:0000:0000:0000:0202:b3ff:fe1e:8329'
|
||||
assert_equal '2001:0db8:85a3:0000:0000:8a2e:0370:7334', request.remote_ip
|
||||
|
||||
request = stub_request 'REMOTE_ADDR' => 'fe80:0000:0000:0000:0202:b3ff:fe1e:8329,::1',
|
||||
'HTTP_X_FORWARDED_FOR' => 'fe80:0000:0000:0000:0202:b3ff:fe1e:8329'
|
||||
assert_equal 'fe80:0000:0000:0000:0202:b3ff:fe1e:8329', request.remote_ip
|
||||
|
||||
request = stub_request 'HTTP_X_FORWARDED_FOR' => 'unknown,fe80:0000:0000:0000:0202:b3ff:fe1e:8329'
|
||||
assert_equal nil, request.remote_ip
|
||||
|
||||
request = stub_request 'HTTP_X_FORWARDED_FOR' => 'fe80:0000:0000:0000:0202:b3ff:fe1e:8329,2001:0db8:85a3:0000:0000:8a2e:0370:7334'
|
||||
assert_equal nil, request.remote_ip
|
||||
end
|
||||
|
||||
test "remote ip with user specified trusted proxies Regexp" do
|
||||
@trusted_proxies = /^67\.205\.106\.73$/i
|
||||
|
||||
@ -128,7 +211,18 @@ def url_for(options = {})
|
||||
assert_equal '3.4.5.6', request.remote_ip
|
||||
|
||||
request = stub_request 'HTTP_X_FORWARDED_FOR' => '67.205.106.73, 10.0.0.1, 9.9.9.9, 3.4.5.6'
|
||||
assert_equal '10.0.0.1', request.remote_ip
|
||||
assert_equal nil, request.remote_ip
|
||||
end
|
||||
|
||||
test "remote ip v6 with user specified trusted proxies Regexp" do
|
||||
@trusted_proxies = /^fe80:0000:0000:0000:0202:b3ff:fe1e:8329$/i
|
||||
|
||||
request = stub_request 'REMOTE_ADDR' => '2001:0db8:85a3:0000:0000:8a2e:0370:7334',
|
||||
'HTTP_X_FORWARDED_FOR' => 'fe80:0000:0000:0000:0202:b3ff:fe1e:8329'
|
||||
assert_equal '2001:0db8:85a3:0000:0000:8a2e:0370:7334', request.remote_ip
|
||||
|
||||
request = stub_request 'HTTP_X_FORWARDED_FOR' => 'fe80:0000:0000:0000:0202:b3ff:fe1e:8329, 2001:0db8:85a3:0000:0000:8a2e:0370:7334'
|
||||
assert_equal nil, request.remote_ip
|
||||
end
|
||||
|
||||
test "domains" do
|
||||
|
Loading…
Reference in New Issue
Block a user