Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix host display when X_FORWARDED_HOST authorized #48941

Merged
merged 1 commit into from Aug 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions actionpack/CHANGELOG.md
@@ -1,3 +1,8 @@
* Fix `HostAuthorization` potentially displaying the value of the
X_FORWARDED_HOST header when the HTTP_HOST header is being blocked.

*Hartley McGuire*, *Daniel Schlosser*

* Rename `fixture_file_upload` method to `file_fixture_upload`

Declare an alias to preserve the backwards compatibility of `fixture_file_upload`
Expand Down
17 changes: 12 additions & 5 deletions actionpack/lib/action_dispatch/middleware/host_authorization.rb
Expand Up @@ -98,7 +98,7 @@ def call(env)
def response_body(request)
return "" unless request.get_header("action_dispatch.show_detailed_exceptions")

template = DebugView.new(host: request.host)
template = DebugView.new(hosts: request.env["action_dispatch.blocked_hosts"])
template.render(template: "rescues/blocked_host", layout: "rescues/layout")
end

Expand All @@ -114,7 +114,7 @@ def log_error(request)

return unless logger

logger.error("[#{self.class.name}] Blocked host: #{request.host}")
logger.error("[#{self.class.name}] Blocked hosts: #{request.env["action_dispatch.blocked_hosts"].join(", ")}")
end

def available_logger(request)
Expand All @@ -134,21 +134,28 @@ def call(env)
return @app.call(env) if @permissions.empty?

request = Request.new(env)
hosts = blocked_hosts(request)

if authorized?(request) || excluded?(request)
if hosts.empty? || excluded?(request)
mark_as_authorized(request)
@app.call(env)
else
env["action_dispatch.blocked_hosts"] = hosts
@response_app.call(env)
end
end

private
def authorized?(request)
def blocked_hosts(request)
hosts = []

origin_host = request.get_header("HTTP_HOST")
hosts << origin_host unless @permissions.allows?(origin_host)

forwarded_host = request.x_forwarded_host&.split(/,\s?/)&.last
hosts << forwarded_host unless forwarded_host.blank? || @permissions.allows?(forwarded_host)

@permissions.allows?(origin_host) && (forwarded_host.blank? || @permissions.allows?(forwarded_host))
hosts
end

def excluded?(request)
Expand Down
@@ -1,8 +1,12 @@
<header>
<h1>Blocked host: <%= @host %></h1>
<h1>Blocked hosts: <%= @hosts.join(", ") %></h1>
</header>
<main role="main" id="container">
<h2>To allow requests to <%= @host %> make sure it is a valid hostname (containing only numbers, letters, dashes and dots), then add the following to your environment configuration:</h2>
<pre>config.hosts &lt;&lt; "<%= @host %>"</pre>
<h2>To allow requests to these hosts, make sure they are valid hostnames (containing only numbers, letters, dashes and dots), then add the following to your environment configuration:</h2>
<pre>
<% @hosts.each do |host| %>
config.hosts &lt;&lt; "<%= host %>"
<% end %>
</pre>
<p>For more details view: <a href="https://guides.rubyonrails.org/configuring.html#actiondispatch-hostauthorization">the Host Authorization guide</a></p>
</main>
@@ -1,7 +1,9 @@
Blocked host: <%= @host %>
Blocked hosts: <%= @hosts.join(", ") %>

To allow requests to <%= @host %> make sure it is a valid hostname (containing only numbers, letters, dashes and dots), then add the following to your environment configuration:
To allow requests to these hosts, make sure they are valid hostnames (containing only numbers, letters, dashes and dots), then add the following to your environment configuration:

config.hosts << "<%= @host %>"
<% @hosts.each do |host| %>
config.hosts << "<%= host %>"
<% end %>

For more details on host authorization view: https://guides.rubyonrails.org/configuring.html#actiondispatch-hostauthorization
32 changes: 16 additions & 16 deletions actionpack/test/dispatch/host_authorization_test.rb
Expand Up @@ -21,7 +21,7 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
get "/", env: { "action_dispatch.show_detailed_exceptions" => true }

assert_response :forbidden
assert_match "Blocked host: www.example.com", response.body
assert_match "Blocked hosts: www.example.com", response.body
end

test "allows all requests if hosts is empty" do
Expand Down Expand Up @@ -93,7 +93,7 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
}

assert_response :forbidden
assert_match "Blocked host: www.example.local", response.body
assert_match "Blocked hosts: www.example.local", response.body
end

test "passes requests to allowed hosts with domain name notation" do
Expand All @@ -114,7 +114,7 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
}

assert_response :forbidden
assert_match "Blocked host: .example.com", response.body
assert_match "Blocked hosts: .example.com", response.body
end

test "checks for requests with #=== to support wider range of host checks" do
Expand All @@ -140,7 +140,7 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
get "/", env: { "action_dispatch.show_detailed_exceptions" => true }

assert_response :forbidden
assert_match "Blocked host: www.example.com", response.body
assert_match "Blocked hosts: www.example.com", response.body
end

test "blocks requests to unallowed host supporting custom responses" do
Expand Down Expand Up @@ -284,7 +284,7 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
}

assert_response :forbidden
assert_match "Blocked host: 127.0.0.1", response.body
assert_match "Blocked hosts: www.example.com", response.body
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This and the other test change demonstrate the issue: the displayed blocked host previously would be HTTP_X_FORWARDED_HOST even though that value was already added to config.hosts. The correct value here should be HTTP_HOST

end

test "blocks requests with spoofed relative X-FORWARDED-HOST" do
Expand All @@ -297,7 +297,7 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
}

assert_response :forbidden
assert_match "Blocked host: //randomhost.com", response.body
assert_match "Blocked hosts: //randomhost.com", response.body
end

test "forwarded secondary hosts are allowed when permitted" do
Expand All @@ -322,7 +322,7 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
}

assert_response :forbidden
assert_match "Blocked host: evil.com", response.body
assert_match "Blocked hosts: evil.com", response.body
end

test "does not consider IP addresses in X-FORWARDED-HOST spoofed when disabled" do
Expand All @@ -347,7 +347,7 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
}

assert_response :forbidden
assert_match "Blocked host: localhost", response.body
assert_match "Blocked hosts: www.example.com", response.body
end

test "forwarded hosts should be permitted" do
Expand All @@ -360,7 +360,7 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
}

assert_response :forbidden
assert_match "Blocked host: sub.domain.com", response.body
assert_match "Blocked hosts: sub.domain.com", response.body
end

test "sub-sub domains should not be permitted" do
Expand All @@ -372,7 +372,7 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
}

assert_response :forbidden
assert_match "Blocked host: secondary.sub.domain.com", response.body
assert_match "Blocked hosts: secondary.sub.domain.com", response.body
end

test "forwarded hosts are allowed when permitted" do
Expand Down Expand Up @@ -420,7 +420,7 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
}

assert_response :forbidden
assert_match "Blocked host: #{host}", response.body
assert_match "Blocked hosts: #{host}", response.body
end
end

Expand All @@ -439,7 +439,7 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
get "/foo", env: { "action_dispatch.show_detailed_exceptions" => true }

assert_response :forbidden
assert_match "Blocked host: www.example.com", response.body
assert_match "Blocked hosts: www.example.com", response.body
end

test "blocks requests with invalid hostnames" do
Expand All @@ -451,7 +451,7 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
}

assert_response :forbidden
assert_match "Blocked host: attacker.com#x.example.com", response.body
assert_match "Blocked hosts: attacker.com#x.example.com", response.body
end

test "blocks requests to similar host" do
Expand All @@ -463,7 +463,7 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
}

assert_response :forbidden
assert_match "Blocked host: sub-example.com", response.body
assert_match "Blocked hosts: sub-example.com", response.body
end

test "uses logger from the env" do
Expand All @@ -473,7 +473,7 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
get "/", env: { "action_dispatch.logger" => Logger.new(output) }

assert_response :forbidden
assert_match "Blocked host: www.example.com", output.rewind && output.read
assert_match "Blocked hosts: www.example.com", output.rewind && output.read
end

test "uses ActionView::Base logger when no logger in the env" do
Expand All @@ -489,7 +489,7 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
end

assert_response :forbidden
assert_match "Blocked host: www.example.com", output.rewind && output.read
assert_match "Blocked hosts: www.example.com", output.rewind && output.read
end

private
Expand Down