1
0
Fork 0
mirror of https://github.com/rails/rails.git synced 2022-11-09 12:12:34 -05:00

Fix invalid forwarded host vulnerability

Prior to this commit, it was possible to pass an unvalidated host
through the `X-Forwarded-Host` header. If the value of the header
was prefixed with a invalid domain character (for example a `/`),
it was always accepted as the actual host of that request.

Since this host is used for all url helpers, an attacker could change
generated links and redirects. If the header is set to
`X-Forwarded-Host: //evil.hacker`, a redirect will be send to
`https:////evil.hacker/`. Browsers will ignore these four slashes
and redirect the user.

[CVE-2021-44528]
This commit is contained in:
Stef Schenkelaars 2021-07-07 12:06:32 +02:00 committed by Aaron Patterson
parent d12eca1f40
commit 0fccfb9a30
No known key found for this signature in database
GPG key ID: 953170BCB4FFAFC6
2 changed files with 91 additions and 8 deletions

View file

@ -52,7 +52,7 @@ module ActionDispatch
def sanitize_string(host)
if host.start_with?(".")
/\A(.+\.)?#{Regexp.escape(host[1..-1])}\z/i
/\A([a-z0-9-]+\.)?#{Regexp.escape(host[1..-1])}\z/i
else
/\A#{Regexp.escape host}\z/i
end
@ -120,13 +120,9 @@ module ActionDispatch
end
private
HOSTNAME = /[a-z0-9.-]+|\[[a-f0-9]*:[a-f0-9.:]+\]/i
VALID_ORIGIN_HOST = /\A(#{HOSTNAME})(?::\d+)?\z/
VALID_FORWARDED_HOST = /(?:\A|,[ ]?)(#{HOSTNAME})(?::\d+)?\z/
def authorized?(request)
origin_host = request.get_header("HTTP_HOST")&.slice(VALID_ORIGIN_HOST, 1) || ""
forwarded_host = request.x_forwarded_host&.slice(VALID_FORWARDED_HOST, 1) || ""
origin_host = request.get_header("HTTP_HOST")
forwarded_host = request.x_forwarded_host&.split(/,\s?/)&.last
@permissions.allows?(origin_host) && (forwarded_host.blank? || @permissions.allows?(forwarded_host))
end

View file

@ -167,6 +167,44 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
assert_match "Blocked host: 127.0.0.1", response.body
end
test "blocks requests with spoofed relative X-FORWARDED-HOST" do
@app = ActionDispatch::HostAuthorization.new(App, ["www.example.com"])
get "/", env: {
"HTTP_X_FORWARDED_HOST" => "//randomhost.com",
"HOST" => "www.example.com",
"action_dispatch.show_detailed_exceptions" => true
}
assert_response :forbidden
assert_match "Blocked host: //randomhost.com", response.body
end
test "forwarded secondary hosts are allowed when permitted" do
@app = ActionDispatch::HostAuthorization.new(App, ".domain.com")
get "/", env: {
"HTTP_X_FORWARDED_HOST" => "example.com, my-sub.domain.com",
"HOST" => "domain.com",
}
assert_response :ok
assert_equal "Success", body
end
test "forwarded secondary hosts are blocked when mismatch" do
@app = ActionDispatch::HostAuthorization.new(App, "domain.com")
get "/", env: {
"HTTP_X_FORWARDED_HOST" => "domain.com, evil.com",
"HOST" => "domain.com",
"action_dispatch.show_detailed_exceptions" => true
}
assert_response :forbidden
assert_match "Blocked host: evil.com", response.body
end
test "does not consider IP addresses in X-FORWARDED-HOST spoofed when disabled" do
@app = ActionDispatch::HostAuthorization.new(App, nil)
@ -205,11 +243,23 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
assert_match "Blocked host: sub.domain.com", response.body
end
test "sub-sub domains should not be permitted" do
@app = ActionDispatch::HostAuthorization.new(App, ".domain.com")
get "/", env: {
"HOST" => "secondary.sub.domain.com",
"action_dispatch.show_detailed_exceptions" => true
}
assert_response :forbidden
assert_match "Blocked host: secondary.sub.domain.com", response.body
end
test "forwarded hosts are allowed when permitted" do
@app = ActionDispatch::HostAuthorization.new(App, ".domain.com")
get "/", env: {
"HTTP_X_FORWARDED_HOST" => "sub.domain.com",
"HTTP_X_FORWARDED_HOST" => "my-sub.domain.com",
"HOST" => "domain.com",
}
@ -217,6 +267,43 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
assert_equal "Success", body
end
test "lots of NG hosts" do
ng_hosts = [
"hacker%E3%80%82com",
"hacker%00.com",
"www.theirsite.com@yoursite.com",
"hacker.com/test/",
"hacker%252ecom",
".hacker.com",
"/\/\/hacker.com/",
"/hacker.com",
"../hacker.com",
".hacker.com",
"@hacker.com",
"hacker.com",
"hacker.com%23@example.com",
"hacker.com/.jpg",
"hacker.com\texample.com/",
"hacker.com/example.com",
"hacker.com\@example.com",
"hacker.com/example.com",
"hacker.com/"
]
@app = ActionDispatch::HostAuthorization.new(App, "example.com")
ng_hosts.each do |host|
get "/", env: {
"HTTP_X_FORWARDED_HOST" => host,
"HOST" => "example.com",
"action_dispatch.show_detailed_exceptions" => true
}
assert_response :forbidden
assert_match "Blocked host: #{host}", response.body
end
end
test "exclude matches allow any host" do
@app = ActionDispatch::HostAuthorization.new(App, "only.com", exclude: ->(req) { req.path == "/foo" })