forked from rails/rails
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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]
- Loading branch information
1 parent
d12eca1
commit 0fccfb9
Showing
2 changed files
with
91 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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,18 +243,67 @@ 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", | ||
} | ||
|
||
assert_response :ok | ||
assert_equal "Success", body | ||
end | ||
|
||
test "lots of NG hosts" do | ||
ng_hosts = [ | ||
"hacker%E3%80%82com", | ||
"hacker%00.com", | ||
"[email protected]", | ||
"hacker.com/test/", | ||
"hacker%252ecom", | ||
".hacker.com", | ||
"/\/\/hacker.com/", | ||
"/hacker.com", | ||
"../hacker.com", | ||
".hacker.com", | ||
"@hacker.com", | ||
"hacker.com", | ||
"hacker.com%[email protected]", | ||
"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" }) | ||
|
||
|