Allow IPs with port in the HostAuthorization middleware

When a host was an IP with a port, the IPAddr object comparisson was
raising an exception which was making the middleware reject the request.

Now we extract the hostname out of the host when comparing with IPAddr
objects.
This commit is contained in:
Rafael Mendonça França 2021-12-15 20:30:08 +00:00
parent 0afa65723a
commit 3a4275fa31
No known key found for this signature in database
GPG Key ID: FC23B6D0F1EEE948
2 changed files with 123 additions and 9 deletions

@ -17,7 +17,15 @@ module ActionDispatch
# if +config.consider_all_requests_local+ is set to true, otherwise the body is empty.
class HostAuthorization
ALLOWED_HOSTS_IN_DEVELOPMENT = [".localhost", IPAddr.new("0.0.0.0/0"), IPAddr.new("::/0")]
PORT_REGEX = /(?::\d+)?/.freeze
PORT_REGEX = /(?::\d+)/ # :nodoc:
IPV4_HOSTNAME = /(?<host>\d+\.\d+\.\d+\.\d+)#{PORT_REGEX}?/ # :nodoc:
IPV6_HOSTNAME = /(?<host>[a-f0-9]*:[a-f0-9.:]+)/i # :nodoc:
IPV6_HOSTNAME_WITH_PORT = /\[#{IPV6_HOSTNAME}\]#{PORT_REGEX}/i # :nodoc:
VALID_IP_HOSTNAME = Regexp.union( # :nodoc:
/\A#{IPV4_HOSTNAME}\z/,
/\A#{IPV6_HOSTNAME}\z/,
/\A#{IPV6_HOSTNAME_WITH_PORT}\z/,
)
class Permissions # :nodoc:
def initialize(hosts)
@ -30,11 +38,17 @@ def empty?
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
if allowed.is_a?(IPAddr)
begin
allowed === extract_hostname(host)
rescue
# IPAddr#=== raises an error if you give it a hostname instead of
# IP. Treat similar errors as blocked access.
false
end
else
allowed === host
end
end
end
@ -50,16 +64,20 @@ def sanitize_hosts(hosts)
end
def sanitize_regexp(host)
/\A#{host}#{PORT_REGEX}\z/
/\A#{host}#{PORT_REGEX}?\z/
end
def sanitize_string(host)
if host.start_with?(".")
/\A([a-z0-9-]+\.)?#{Regexp.escape(host[1..-1])}#{PORT_REGEX}\z/i
/\A([a-z0-9-]+\.)?#{Regexp.escape(host[1..-1])}#{PORT_REGEX}?\z/i
else
/\A#{Regexp.escape host}#{PORT_REGEX}\z/i
/\A#{Regexp.escape host}#{PORT_REGEX}?\z/i
end
end
def extract_hostname(host)
host.slice(VALID_IP_HOSTNAME, "host") || host
end
end
class DefaultResponseApp # :nodoc:

@ -166,6 +166,102 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
assert_match "Success", response.body
end
test "localhost using IPV4 works in dev" do
@app = ActionDispatch::HostAuthorization.new(App, ActionDispatch::HostAuthorization::ALLOWED_HOSTS_IN_DEVELOPMENT)
get "/", env: {
"HOST" => "127.0.0.1",
"action_dispatch.show_detailed_exceptions" => true
}
assert_response :ok
assert_match "Success", response.body
end
test "localhost using IPV4 with port works in dev" do
@app = ActionDispatch::HostAuthorization.new(App, ActionDispatch::HostAuthorization::ALLOWED_HOSTS_IN_DEVELOPMENT)
get "/", env: {
"HOST" => "127.0.0.1:3000",
"action_dispatch.show_detailed_exceptions" => true
}
assert_response :ok
assert_match "Success", response.body
end
test "localhost using IPV4 binding in all addresses works in dev" do
@app = ActionDispatch::HostAuthorization.new(App, ActionDispatch::HostAuthorization::ALLOWED_HOSTS_IN_DEVELOPMENT)
get "/", env: {
"HOST" => "0.0.0.0",
"action_dispatch.show_detailed_exceptions" => true
}
assert_response :ok
assert_match "Success", response.body
end
test "localhost using IPV4 with port binding in all addresses works in dev" do
@app = ActionDispatch::HostAuthorization.new(App, ActionDispatch::HostAuthorization::ALLOWED_HOSTS_IN_DEVELOPMENT)
get "/", env: {
"HOST" => "0.0.0.0:3000",
"action_dispatch.show_detailed_exceptions" => true
}
assert_response :ok
assert_match "Success", response.body
end
test "localhost using IPV6 works in dev" do
@app = ActionDispatch::HostAuthorization.new(App, ActionDispatch::HostAuthorization::ALLOWED_HOSTS_IN_DEVELOPMENT)
get "/", env: {
"HOST" => "::1",
"action_dispatch.show_detailed_exceptions" => true
}
assert_response :ok
assert_match "Success", response.body
end
test "localhost using IPV6 with port works in dev" do
@app = ActionDispatch::HostAuthorization.new(App, ActionDispatch::HostAuthorization::ALLOWED_HOSTS_IN_DEVELOPMENT)
get "/", env: {
"HOST" => "[::1]:3000",
"action_dispatch.show_detailed_exceptions" => true
}
assert_response :ok
assert_match "Success", response.body
end
test "localhost using IPV6 binding in all addresses works in dev" do
@app = ActionDispatch::HostAuthorization.new(App, ActionDispatch::HostAuthorization::ALLOWED_HOSTS_IN_DEVELOPMENT)
get "/", env: {
"HOST" => "::",
"action_dispatch.show_detailed_exceptions" => true
}
assert_response :ok
assert_match "Success", response.body
end
test "localhost using IPV6 with port binding in all addresses works in dev" do
@app = ActionDispatch::HostAuthorization.new(App, ActionDispatch::HostAuthorization::ALLOWED_HOSTS_IN_DEVELOPMENT)
get "/", env: {
"HOST" => "[::]:3000",
"action_dispatch.show_detailed_exceptions" => true
}
assert_response :ok
assert_match "Success", response.body
end
test "hosts with port works" do
@app = ActionDispatch::HostAuthorization.new(App, ["host.test"])