mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
Add request exclusion to Host Authorization
In the same way that requests may need to be excluded from forced SSL, requests may also need to be excluded from the Host Authorization checks. By providing this additional flexibility more applications will be able to enable Host Authorization while excluding requests that may not conform. For example, AWS Classic Load Balancers don't provide a Host header and cannot be configured to send one. This means that Host Authorization must be disabled to use the health check provided by the load balancer. This change will allow an application to exclude the health check requests from the Host Authorization requirements. I've modified the `ActionDispatch::HostAuthorization` middleware to accept arguments in a similar way to `ActionDispatch::SSL`. The hosts configuration setting still exists separately as does the hosts_response_app but I've tried to group the Host Authorization settings like the ssl_options. It may make sense to deprecate the global hosts_response_app if it's only used as part of the Host Authorization failure response. I've also updated the existing tests as the method signature changed and added new tests to verify the exclusion functionality.
This commit is contained in:
parent
872e757145
commit
1f767407cb
5 changed files with 58 additions and 7 deletions
|
@ -1,3 +1,9 @@
|
|||
* Allow `ActionDispatch::HostAuthorization` to exclude specific requests.
|
||||
|
||||
Host Authorization checks can be skipped for specific requests. This allows for health check requests to be permitted for requests with missing or non-matching host headers.
|
||||
|
||||
*Chris Bisnett*
|
||||
|
||||
* Add `config.action_dispatch.request_id_header` to allow changing the name of
|
||||
the unique X-Request-Id header
|
||||
|
||||
|
|
|
@ -4,7 +4,12 @@ require "action_dispatch/http/request"
|
|||
|
||||
module ActionDispatch
|
||||
# This middleware guards from DNS rebinding attacks by explicitly permitting
|
||||
# the hosts a request can be sent to.
|
||||
# the hosts a request can be sent to, and is passed the options set in
|
||||
# +config.host_authorization+.
|
||||
#
|
||||
# Requests can opt-out of Host Authorization with +exclude+:
|
||||
#
|
||||
# config.host_authorization = { exclude: ->(request) { request.path =~ /healthcheck/ } }
|
||||
#
|
||||
# When a request comes to an unauthorized host, the +response_app+
|
||||
# application will be executed and rendered. If no +response_app+ is given, a
|
||||
|
@ -66,9 +71,20 @@ module ActionDispatch
|
|||
}, [body]]
|
||||
end
|
||||
|
||||
def initialize(app, hosts, response_app = nil)
|
||||
def initialize(app, hosts, deprecated_response_app = nil, exclude: nil, response_app: nil)
|
||||
@app = app
|
||||
@permissions = Permissions.new(hosts)
|
||||
@exclude = exclude
|
||||
|
||||
unless deprecated_response_app.nil?
|
||||
ActiveSupport::Deprecation.warn(<<-MSG.squish)
|
||||
`action_dispatch.hosts_response_app` is deprecated and will be ignored in Rails 6.2.
|
||||
Use the Host Authorization `response_app` setting instead.
|
||||
MSG
|
||||
|
||||
response_app ||= deprecated_response_app
|
||||
end
|
||||
|
||||
@response_app = response_app || DEFAULT_RESPONSE_APP
|
||||
end
|
||||
|
||||
|
@ -77,7 +93,7 @@ module ActionDispatch
|
|||
|
||||
request = Request.new(env)
|
||||
|
||||
if authorized?(request)
|
||||
if authorized?(request) || excluded?(request)
|
||||
mark_as_authorized(request)
|
||||
@app.call(env)
|
||||
else
|
||||
|
@ -94,6 +110,10 @@ module ActionDispatch
|
|||
(forwarded_host.blank? || @permissions.allows?(forwarded_host))
|
||||
end
|
||||
|
||||
def excluded?(request)
|
||||
@exclude && @exclude.call(request)
|
||||
end
|
||||
|
||||
def mark_as_authorized(request)
|
||||
request.set_header("action_dispatch.authorized_host", request.host)
|
||||
end
|
||||
|
|
|
@ -89,7 +89,7 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
|
|||
end
|
||||
|
||||
test "blocks requests to unallowed host supporting custom responses" do
|
||||
@app = ActionDispatch::HostAuthorization.new(App, ["w.example.co"], -> env do
|
||||
@app = ActionDispatch::HostAuthorization.new(App, ["w.example.co"], response_app: -> env do
|
||||
[401, {}, %w(Custom)]
|
||||
end)
|
||||
|
||||
|
@ -158,4 +158,28 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
|
|||
assert_response :ok
|
||||
assert_equal "Success", body
|
||||
end
|
||||
|
||||
test "exclude matches allow any host" do
|
||||
@app = ActionDispatch::HostAuthorization.new(App, "only.com", exclude: ->(req) { req.path == "/foo" })
|
||||
|
||||
get "/foo"
|
||||
|
||||
assert_response :ok
|
||||
assert_equal "Success", body
|
||||
end
|
||||
|
||||
test "exclude misses block unallowed hosts" do
|
||||
@app = ActionDispatch::HostAuthorization.new(App, "only.com", exclude: ->(req) { req.path == "/bar" })
|
||||
|
||||
get "/foo"
|
||||
|
||||
assert_response :forbidden
|
||||
assert_match "Blocked host: www.example.com", response.body
|
||||
end
|
||||
|
||||
test "config setting action_dispatch.hosts_response_app is deprecated" do
|
||||
assert_deprecated do
|
||||
ActionDispatch::HostAuthorization.new(App, "example.com", ->(env) { true })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -14,8 +14,8 @@ module Rails
|
|||
attr_accessor :allow_concurrency, :asset_host, :autoflush_log,
|
||||
:cache_classes, :cache_store, :consider_all_requests_local, :console,
|
||||
:eager_load, :exceptions_app, :file_watcher, :filter_parameters,
|
||||
:force_ssl, :helpers_paths, :hosts, :logger, :log_formatter, :log_tags,
|
||||
:railties_order, :relative_url_root, :secret_key_base,
|
||||
:force_ssl, :helpers_paths, :hosts, :host_authorization, :logger, :log_formatter,
|
||||
:log_tags, :railties_order, :relative_url_root, :secret_key_base,
|
||||
:ssl_options, :public_file_server,
|
||||
:session_options, :time_zone, :reload_classes_only_on_change,
|
||||
:beginning_of_week, :filter_redirect, :x, :enable_dependency_loading,
|
||||
|
@ -35,6 +35,7 @@ module Rails
|
|||
@filter_redirect = []
|
||||
@helpers_paths = []
|
||||
@hosts = Array(([".localhost", IPAddr.new("0.0.0.0/0"), IPAddr.new("::/0")] if Rails.env.development?))
|
||||
@host_authorization = {}
|
||||
@public_file_server = ActiveSupport::OrderedOptions.new
|
||||
@public_file_server.enabled = true
|
||||
@public_file_server.index_name = "index"
|
||||
|
|
|
@ -13,7 +13,7 @@ module Rails
|
|||
|
||||
def build_stack
|
||||
ActionDispatch::MiddlewareStack.new do |middleware|
|
||||
middleware.use ::ActionDispatch::HostAuthorization, config.hosts, config.action_dispatch.hosts_response_app
|
||||
middleware.use ::ActionDispatch::HostAuthorization, config.hosts, config.action_dispatch.hosts_response_app, **config.host_authorization
|
||||
|
||||
if config.force_ssl
|
||||
middleware.use ::ActionDispatch::SSL, **config.ssl_options,
|
||||
|
|
Loading…
Reference in a new issue