Merge pull request #38829 from cbisnett/host_authorization_exclude

Add request exclusion to Host Authorization
This commit is contained in:
Eugene Kenny 2020-11-02 20:22:35 +00:00 committed by GitHub
commit 67e69e8db4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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 @@
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 @@ def sanitize_string(host)
}, [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 @@ def call(env)
request = Request.new(env)
if authorized?(request)
if authorized?(request) || excluded?(request)
mark_as_authorized(request)
@app.call(env)
else
@ -94,6 +110,10 @@ def authorized?(request)
(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 @@ class Configuration < ::Rails::Engine::Configuration
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 @@ def initialize(*)
@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 @@ def initialize(app, config, paths)
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,