mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
Move remote_ip to a middleware:
* ActionController::Base.ip_spoofing_check deprecated => config.action_dispatch.ip_spoofing_check * ActionController::Base.trusted_proxies deprecated => config.action_dispatch.trusted_proxies
This commit is contained in:
parent
9a9caf646d
commit
93422af5d5
9 changed files with 118 additions and 63 deletions
|
@ -42,13 +42,13 @@ module ActionDispatch
|
||||||
end
|
end
|
||||||
|
|
||||||
autoload_under 'middleware' do
|
autoload_under 'middleware' do
|
||||||
autoload :BlockUntrustedIps
|
|
||||||
autoload :Callbacks
|
autoload :Callbacks
|
||||||
autoload :Cascade
|
autoload :Cascade
|
||||||
autoload :Cookies
|
autoload :Cookies
|
||||||
autoload :Flash
|
autoload :Flash
|
||||||
autoload :Head
|
autoload :Head
|
||||||
autoload :ParamsParser
|
autoload :ParamsParser
|
||||||
|
autoload :RemoteIp
|
||||||
autoload :Rescue
|
autoload :Rescue
|
||||||
autoload :ShowExceptions
|
autoload :ShowExceptions
|
||||||
autoload :Static
|
autoload :Static
|
||||||
|
|
|
@ -119,36 +119,7 @@ module ActionDispatch
|
||||||
# delimited list in the case of multiple chained proxies; the last
|
# delimited list in the case of multiple chained proxies; the last
|
||||||
# address which is not trusted is the originating IP.
|
# address which is not trusted is the originating IP.
|
||||||
def remote_ip
|
def remote_ip
|
||||||
remote_addr_list = @env['REMOTE_ADDR'] && @env['REMOTE_ADDR'].scan(/[^,\s]+/)
|
(@env["action_dispatch.remote_ip"] || ip).to_s
|
||||||
|
|
||||||
unless remote_addr_list.blank?
|
|
||||||
not_trusted_addrs = remote_addr_list.reject {|addr| addr =~ TRUSTED_PROXIES || addr =~ ActionController::Base.trusted_proxies}
|
|
||||||
return not_trusted_addrs.first unless not_trusted_addrs.empty?
|
|
||||||
end
|
|
||||||
remote_ips = @env['HTTP_X_FORWARDED_FOR'] && @env['HTTP_X_FORWARDED_FOR'].split(',')
|
|
||||||
|
|
||||||
if @env.include? 'HTTP_CLIENT_IP'
|
|
||||||
if ActionController::Base.ip_spoofing_check && remote_ips && !remote_ips.include?(@env['HTTP_CLIENT_IP'])
|
|
||||||
# We don't know which came from the proxy, and which from the user
|
|
||||||
raise ActionController::ActionControllerError.new <<EOM
|
|
||||||
IP spoofing attack?!
|
|
||||||
HTTP_CLIENT_IP=#{@env['HTTP_CLIENT_IP'].inspect}
|
|
||||||
HTTP_X_FORWARDED_FOR=#{@env['HTTP_X_FORWARDED_FOR'].inspect}
|
|
||||||
EOM
|
|
||||||
end
|
|
||||||
|
|
||||||
return @env['HTTP_CLIENT_IP']
|
|
||||||
end
|
|
||||||
|
|
||||||
if remote_ips
|
|
||||||
while remote_ips.size > 1 && (TRUSTED_PROXIES =~ remote_ips.last.strip || ActionController::Base.trusted_proxies =~ remote_ips.last.strip)
|
|
||||||
remote_ips.pop
|
|
||||||
end
|
|
||||||
|
|
||||||
return remote_ips.last.strip
|
|
||||||
end
|
|
||||||
|
|
||||||
@env['REMOTE_ADDR']
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Returns the lowercase name of the HTTP server software.
|
# Returns the lowercase name of the HTTP server software.
|
||||||
|
|
|
@ -1,25 +0,0 @@
|
||||||
module ActionDispatch
|
|
||||||
class BlockUntrustedIps
|
|
||||||
class SpoofAttackError < StandardError ; end
|
|
||||||
|
|
||||||
def initialize(app)
|
|
||||||
@app = app
|
|
||||||
end
|
|
||||||
|
|
||||||
def call(env)
|
|
||||||
if @env['HTTP_X_FORWARDED_FOR'] && @env['HTTP_CLIENT_IP']
|
|
||||||
remote_ips = @env['HTTP_X_FORWARDED_FOR'].split(',')
|
|
||||||
|
|
||||||
unless remote_ips.include?(@env['HTTP_CLIENT_IP'])
|
|
||||||
http_client_ip = @env['HTTP_CLIENT_IP'].inspect
|
|
||||||
http_forwarded_for = @env['HTTP_X_FORWARDED_FOR'].inspect
|
|
||||||
|
|
||||||
raise SpoofAttackError, "IP spoofing attack?!\n " \
|
|
||||||
"HTTP_CLIENT_IP=#{http_client_ip}\n HTTP_X_FORWARDED_FOR=http_forwarded_for"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@app.call(env)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
51
actionpack/lib/action_dispatch/middleware/remote_ip.rb
Normal file
51
actionpack/lib/action_dispatch/middleware/remote_ip.rb
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
module ActionDispatch
|
||||||
|
class RemoteIp
|
||||||
|
class IpSpoofAttackError < StandardError ; end
|
||||||
|
|
||||||
|
class RemoteIpGetter
|
||||||
|
def initialize(env, check_ip_spoofing, trusted_proxies)
|
||||||
|
@env = env
|
||||||
|
@check_ip_spoofing = check_ip_spoofing
|
||||||
|
@trusted_proxies = trusted_proxies
|
||||||
|
end
|
||||||
|
|
||||||
|
def remote_addrs
|
||||||
|
@remote_addrs ||= begin
|
||||||
|
list = @env['REMOTE_ADDR'] ? @env['REMOTE_ADDR'].split(/[,\s]+/) : []
|
||||||
|
list.reject { |addr| addr =~ @trusted_proxies }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_s
|
||||||
|
return remote_addrs.first if remote_addrs.any?
|
||||||
|
|
||||||
|
forwarded_ips = @env['HTTP_X_FORWARDED_FOR'] ? @env['HTTP_X_FORWARDED_FOR'].strip.split(/[,\s]+/) : []
|
||||||
|
|
||||||
|
if client_ip = @env['HTTP_CLIENT_IP']
|
||||||
|
if @check_ip_spoofing && !forwarded_ips.include?(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
|
||||||
|
return client_ip
|
||||||
|
end
|
||||||
|
|
||||||
|
return forwarded_ips.reject { |ip| ip =~ @trusted_proxies }.last || @env["REMOTE_ADDR"]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize(app, check_ip_spoofing = true, trusted_proxies = nil)
|
||||||
|
@app = app
|
||||||
|
@check_ip_spoofing = check_ip_spoofing
|
||||||
|
regex = '(^127\.0\.0\.1$|^(10|172\.(1[6-9]|2[0-9]|30|31)|192\.168)\.)'
|
||||||
|
regex << "|(#{trusted_proxies})" if trusted_proxies
|
||||||
|
@trusted_proxies = Regexp.new(regex, "i")
|
||||||
|
end
|
||||||
|
|
||||||
|
def call(env)
|
||||||
|
env["action_dispatch.remote_ip"] = RemoteIpGetter.new(env, @check_ip_spoofing, @trusted_proxies)
|
||||||
|
@app.call(env)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -6,6 +6,7 @@ module ActionDispatch
|
||||||
railtie_name :action_dispatch
|
railtie_name :action_dispatch
|
||||||
|
|
||||||
config.action_dispatch.x_sendfile_header = "X-Sendfile"
|
config.action_dispatch.x_sendfile_header = "X-Sendfile"
|
||||||
|
config.action_dispatch.ip_spoofing_check = true
|
||||||
|
|
||||||
# Prepare dispatcher callbacks and run 'prepare' callbacks
|
# Prepare dispatcher callbacks and run 'prepare' callbacks
|
||||||
initializer "action_dispatch.prepare_dispatcher" do |app|
|
initializer "action_dispatch.prepare_dispatcher" do |app|
|
||||||
|
|
|
@ -42,7 +42,7 @@ class RequestTest < ActiveSupport::TestCase
|
||||||
|
|
||||||
request = stub_request 'HTTP_X_FORWARDED_FOR' => '1.1.1.1',
|
request = stub_request 'HTTP_X_FORWARDED_FOR' => '1.1.1.1',
|
||||||
'HTTP_CLIENT_IP' => '2.2.2.2'
|
'HTTP_CLIENT_IP' => '2.2.2.2'
|
||||||
e = assert_raise(ActionController::ActionControllerError) {
|
e = assert_raise(ActionDispatch::RemoteIp::IpSpoofAttackError) {
|
||||||
request.remote_ip
|
request.remote_ip
|
||||||
}
|
}
|
||||||
assert_match /IP spoofing attack/, e.message
|
assert_match /IP spoofing attack/, e.message
|
||||||
|
@ -54,18 +54,17 @@ class RequestTest < ActiveSupport::TestCase
|
||||||
# example is WAP. Since the cellular network is not IP based, it's a
|
# 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
|
# leap of faith to assume that their proxies are ever going to set the
|
||||||
# HTTP_CLIENT_IP/HTTP_X_FORWARDED_FOR headers properly.
|
# HTTP_CLIENT_IP/HTTP_X_FORWARDED_FOR headers properly.
|
||||||
ActionController::Base.ip_spoofing_check = false
|
|
||||||
request = stub_request 'HTTP_X_FORWARDED_FOR' => '1.1.1.1',
|
request = stub_request 'HTTP_X_FORWARDED_FOR' => '1.1.1.1',
|
||||||
'HTTP_CLIENT_IP' => '2.2.2.2'
|
'HTTP_CLIENT_IP' => '2.2.2.2',
|
||||||
|
:ip_spoofing_check => false
|
||||||
assert_equal '2.2.2.2', request.remote_ip
|
assert_equal '2.2.2.2', request.remote_ip
|
||||||
ActionController::Base.ip_spoofing_check = true
|
|
||||||
|
|
||||||
request = stub_request 'HTTP_X_FORWARDED_FOR' => '8.8.8.8, 9.9.9.9'
|
request = stub_request 'HTTP_X_FORWARDED_FOR' => '8.8.8.8, 9.9.9.9'
|
||||||
assert_equal '9.9.9.9', request.remote_ip
|
assert_equal '9.9.9.9', request.remote_ip
|
||||||
end
|
end
|
||||||
|
|
||||||
test "remote ip with user specified trusted proxies" do
|
test "remote ip with user specified trusted proxies" do
|
||||||
ActionController::Base.trusted_proxies = /^67\.205\.106\.73$/i
|
@trusted_proxies = /^67\.205\.106\.73$/i
|
||||||
|
|
||||||
request = stub_request 'REMOTE_ADDR' => '67.205.106.73',
|
request = stub_request 'REMOTE_ADDR' => '67.205.106.73',
|
||||||
'HTTP_X_FORWARDED_FOR' => '3.4.5.6'
|
'HTTP_X_FORWARDED_FOR' => '3.4.5.6'
|
||||||
|
@ -429,6 +428,9 @@ class RequestTest < ActiveSupport::TestCase
|
||||||
protected
|
protected
|
||||||
|
|
||||||
def stub_request(env = {})
|
def stub_request(env = {})
|
||||||
|
ip_spoofing_check = env.key?(:ip_spoofing_check) ? env.delete(:ip_spoofing_check) : true
|
||||||
|
ip_app = ActionDispatch::RemoteIp.new(Proc.new { }, ip_spoofing_check, @trusted_proxies)
|
||||||
|
ip_app.call(env)
|
||||||
ActionDispatch::Request.new(env)
|
ActionDispatch::Request.new(env)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -84,11 +84,12 @@ module Rails
|
||||||
middleware.use('::Rack::Runtime')
|
middleware.use('::Rack::Runtime')
|
||||||
middleware.use('::Rails::Rack::Logger')
|
middleware.use('::Rails::Rack::Logger')
|
||||||
middleware.use('::ActionDispatch::ShowExceptions', lambda { consider_all_requests_local })
|
middleware.use('::ActionDispatch::ShowExceptions', lambda { consider_all_requests_local })
|
||||||
|
middleware.use("::ActionDispatch::RemoteIp", lambda { action_dispatch.ip_spoofing_check }, lambda { action_dispatch.trusted_proxies })
|
||||||
middleware.use('::Rack::Sendfile', lambda { action_dispatch.x_sendfile_header })
|
middleware.use('::Rack::Sendfile', lambda { action_dispatch.x_sendfile_header })
|
||||||
middleware.use('::ActionDispatch::Callbacks', lambda { !cache_classes })
|
middleware.use('::ActionDispatch::Callbacks', lambda { !cache_classes })
|
||||||
middleware.use('::ActionDispatch::Cookies')
|
middleware.use('::ActionDispatch::Cookies')
|
||||||
middleware.use(lambda { ActionController::Base.session_store }, lambda { ActionController::Base.session_options })
|
middleware.use(lambda { action_controller.session_store }, lambda { action_controller.session_options })
|
||||||
middleware.use('::ActionDispatch::Flash', :if => lambda { ActionController::Base.session_store })
|
middleware.use('::ActionDispatch::Flash', :if => lambda { action_controller.session_store })
|
||||||
middleware.use(lambda { metal_loader.build_middleware(metals) }, :if => lambda { metal_loader.metals.any? })
|
middleware.use(lambda { metal_loader.build_middleware(metals) }, :if => lambda { metal_loader.metals.any? })
|
||||||
middleware.use('ActionDispatch::ParamsParser')
|
middleware.use('ActionDispatch::ParamsParser')
|
||||||
middleware.use('::Rack::MethodOverride')
|
middleware.use('::Rack::MethodOverride')
|
||||||
|
|
|
@ -87,6 +87,7 @@ module Rails
|
||||||
%w(info debug warn error fatal unknown).each do |level|
|
%w(info debug warn error fatal unknown).each do |level|
|
||||||
class_eval <<-METHOD, __FILE__, __LINE__ + 1
|
class_eval <<-METHOD, __FILE__, __LINE__ + 1
|
||||||
def #{level}(*args, &block)
|
def #{level}(*args, &block)
|
||||||
|
return unless logger
|
||||||
logger.#{level}(*args, &block)
|
logger.#{level}(*args, &block)
|
||||||
end
|
end
|
||||||
METHOD
|
METHOD
|
||||||
|
|
53
railties/test/application/middleware_stack_defaults_test.rb
Normal file
53
railties/test/application/middleware_stack_defaults_test.rb
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
require 'isolation/abstract_unit'
|
||||||
|
|
||||||
|
class MiddlewareStackDefaultsTest < Test::Unit::TestCase
|
||||||
|
include ActiveSupport::Testing::Isolation
|
||||||
|
|
||||||
|
def setup
|
||||||
|
boot_rails
|
||||||
|
require "rails"
|
||||||
|
require "action_controller/railtie"
|
||||||
|
|
||||||
|
Object.const_set(:MyApplication, Class.new(Rails::Application))
|
||||||
|
MyApplication.class_eval do
|
||||||
|
config.action_controller.session = { :key => "_myapp_session", :secret => "OMG A SEKRET" * 10 }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def remote_ip(env = {})
|
||||||
|
remote_ip = nil
|
||||||
|
env = Rack::MockRequest.env_for("/").merge(env).merge('action_dispatch.show_exceptions' => false)
|
||||||
|
|
||||||
|
endpoint = Proc.new do |e|
|
||||||
|
remote_ip = ActionDispatch::Request.new(e).remote_ip
|
||||||
|
[200, {}, ["Hello"]]
|
||||||
|
end
|
||||||
|
|
||||||
|
out = MyApplication.middleware.build(endpoint).call(env)
|
||||||
|
remote_ip
|
||||||
|
end
|
||||||
|
|
||||||
|
test "remote_ip works" do
|
||||||
|
assert_equal "1.1.1.1", remote_ip("REMOTE_ADDR" => "1.1.1.1")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "checks IP spoofing by default" do
|
||||||
|
assert_raises(ActionDispatch::RemoteIp::IpSpoofAttackError) do
|
||||||
|
remote_ip("HTTP_X_FORWARDED_FOR" => "1.1.1.1", "HTTP_CLIENT_IP" => "1.1.1.2")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "can disable IP spoofing check" do
|
||||||
|
MyApplication.config.action_dispatch.ip_spoofing_check = false
|
||||||
|
|
||||||
|
assert_nothing_raised(ActionDispatch::RemoteIp::IpSpoofAttackError) do
|
||||||
|
assert_equal "1.1.1.2", remote_ip("HTTP_X_FORWARDED_FOR" => "1.1.1.1", "HTTP_CLIENT_IP" => "1.1.1.2")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "the user can set trusted proxies" do
|
||||||
|
MyApplication.config.action_dispatch.trusted_proxies = /^4\.2\.42\.42$/
|
||||||
|
|
||||||
|
assert_equal "1.1.1.1", remote_ip("REMOTE_ADDR" => "4.2.42.42,1.1.1.1")
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in a new issue