mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
1f767407cb
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.
121 lines
3.4 KiB
Ruby
121 lines
3.4 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
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, 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
|
|
# default one will run, which responds with +403 Forbidden+.
|
|
class HostAuthorization
|
|
class Permissions # :nodoc:
|
|
def initialize(hosts)
|
|
@hosts = sanitize_hosts(hosts)
|
|
end
|
|
|
|
def empty?
|
|
@hosts.empty?
|
|
end
|
|
|
|
def allows?(host)
|
|
@hosts.any? do |allowed|
|
|
allowed === host
|
|
rescue
|
|
# IPAddr#=== raises an error if you give it a hostname instead of
|
|
# IP. Treat similar errors as blocked access.
|
|
false
|
|
end
|
|
end
|
|
|
|
private
|
|
def sanitize_hosts(hosts)
|
|
Array(hosts).map do |host|
|
|
case host
|
|
when Regexp then sanitize_regexp(host)
|
|
when String then sanitize_string(host)
|
|
else host
|
|
end
|
|
end
|
|
end
|
|
|
|
def sanitize_regexp(host)
|
|
/\A#{host}\z/
|
|
end
|
|
|
|
def sanitize_string(host)
|
|
if host.start_with?(".")
|
|
/\A(.+\.)?#{Regexp.escape(host[1..-1])}\z/
|
|
else
|
|
host
|
|
end
|
|
end
|
|
end
|
|
|
|
DEFAULT_RESPONSE_APP = -> env do
|
|
request = Request.new(env)
|
|
|
|
format = request.xhr? ? "text/plain" : "text/html"
|
|
template = DebugView.new(host: request.host)
|
|
body = template.render(template: "rescues/blocked_host", layout: "rescues/layout")
|
|
|
|
[403, {
|
|
"Content-Type" => "#{format}; charset=#{Response.default_charset}",
|
|
"Content-Length" => body.bytesize.to_s,
|
|
}, [body]]
|
|
end
|
|
|
|
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
|
|
|
|
def call(env)
|
|
return @app.call(env) if @permissions.empty?
|
|
|
|
request = Request.new(env)
|
|
|
|
if authorized?(request) || excluded?(request)
|
|
mark_as_authorized(request)
|
|
@app.call(env)
|
|
else
|
|
@response_app.call(env)
|
|
end
|
|
end
|
|
|
|
private
|
|
def authorized?(request)
|
|
origin_host = request.get_header("HTTP_HOST").to_s.sub(/:\d+\z/, "")
|
|
forwarded_host = request.x_forwarded_host.to_s.split(/,\s?/).last.to_s.sub(/:\d+\z/, "")
|
|
|
|
@permissions.allows?(origin_host) &&
|
|
(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
|
|
end
|
|
end
|