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

Add HostAuthorization rack-protection middleware #2053

Merged
merged 34 commits into from
Nov 18, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
fb2d76d
Add `permitted_hosts` setting
dentarg Nov 5, 2024
b5df6e1
Split bad/good cases in `HostAuthorization` specs
dentarg Nov 8, 2024
492589f
Less brittle `HostAuthorization` specs
dentarg Nov 8, 2024
841ee31
Simplify `HostAuthorization` tests for Sinatra
dentarg Nov 8, 2024
d2293d9
Document `HostAuthorization` in rack-protection README
dentarg Nov 8, 2024
c9c2625
Expose all the options for `HostAuthorization`
dentarg Nov 8, 2024
6e3711e
Test with multiple hostnames in headers
dentarg Nov 8, 2024
1701645
Improve test descriptions
dentarg Nov 8, 2024
071358e
Port should not matter for `HostAuthorization`
dentarg Nov 8, 2024
856d8e6
No need for `Rack::Protection::HostAuthorization.forwarded?` now
dentarg Nov 8, 2024
ffaf412
Support for `IPAddr` hosts
dentarg Nov 8, 2024
44dd856
Always look at the `Host` header
dentarg Nov 12, 2024
76e614a
Better grouping of `HostAuthorization` specs
dentarg Nov 12, 2024
e6264be
Test with more exotic header values
dentarg Nov 12, 2024
d179c0c
Support for subdomains
dentarg Nov 12, 2024
4652ad2
Support `Forwarded` header in `#forwarded?`
dentarg Nov 12, 2024
a167d8b
Use the same `require` order as other protections
dentarg Nov 13, 2024
3d5383d
Better `allow_if` example in tests
dentarg Nov 13, 2024
dd73d0e
`HostAuthorization` development settings
dentarg Nov 13, 2024
8466a9e
Test with `nil` `HTTP_HOST`
dentarg Nov 13, 2024
1081bf8
Fix typo in README
dentarg Nov 14, 2024
85d2079
Mention `IPAddr` objects
dentarg Nov 14, 2024
801e821
Add (optional) debug logging in `HostAuthorization`
dentarg Nov 14, 2024
579a2e6
Fix incorrect test description
dentarg Nov 15, 2024
affc961
Use keyword argument for `mock_app` helper
dentarg Nov 15, 2024
543bac2
Better test description
dentarg Nov 15, 2024
b1bf256
Fix typo in test description
dentarg Nov 15, 2024
9f0d130
Reject invalid hostnames
dentarg Nov 17, 2024
5947a6d
Document `host_authorization` defaults
dentarg Nov 17, 2024
b29d698
Lint `HostAuthorization` middleware
dentarg Nov 18, 2024
5ba1250
Lint `HostAuthorization` specs
dentarg Nov 18, 2024
3059727
Avoid `warning: character class has '-' without escape: ...`
dentarg Nov 18, 2024
4dfd160
Silence `Style/SafeNavigationChainLength` cop
dentarg Nov 18, 2024
7697337
No need to silence `SafeNavigationChainLength` cop
dentarg Nov 18, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Port should not matter for HostAuthorization
  • Loading branch information
dentarg committed Nov 8, 2024
commit 071358e54aeb84e1193eb62856db69689278913f
6 changes: 5 additions & 1 deletion lib/sinatra/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,11 @@ def uri(addr = nil, absolute = true, add_script_name = true)
uri = [host = String.new]
if absolute
host << "http#{'s' if request.secure?}://"
host << Rack::Protection::HostAuthorization.host_from(request: request)
host << if request.forwarded? || (request.port != (request.secure? ? 443 : 80))
request.host_with_port
else
request.host
end
end
uri << request.script_name.to_s if add_script_name
uri << (addr || request.path_info).to_s
Expand Down
10 changes: 1 addition & 9 deletions rack-protection/lib/rack/protection/host_authorization.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,6 @@ def self.forwarded?(request)
request.get_header(Request::HTTP_X_FORWARDED_HOST)
end

def self.host_from(request:)
if forwarded?(request) || (request.port != (request.ssl? ? 443 : 80))
request.host_with_port
else
request.host
end
end

def initialize(*)
super
@permitted_hosts = Array(options[:permitted_hosts]).map(&:downcase)
Expand All @@ -45,7 +37,7 @@ def accepts?(env)
return true if @permitted_hosts.empty?

request = Request.new(env)
origin_host = self.class.host_from(request: request)
origin_host = request.host

@permitted_hosts.include?(origin_host.downcase)
end
Expand Down
Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Member Author

@dentarg dentarg Nov 15, 2024

Choose a reason for hiding this comment

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

I will just copy the tests here for easier discussion (no need to open the link every time you visit this convo)

https://github.com/rails/rails/blob/6230bd334ab47f9efc6d97c6627abe76c80d8058/actionpack/test/dispatch/host_authorization_test.rb#L108

  test "does not allow domain name notation in the HOST header itself" do
    @app = build_app(".example.com")

    get "/", env: {
      "HOST" => ".example.com",
      "action_dispatch.show_detailed_exceptions" => true
    }

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

https://github.com/rails/rails/blob/6230bd334ab47f9efc6d97c6627abe76c80d8058/actionpack/test/dispatch/host_authorization_test.rb#L449

  test "blocks requests with invalid hostnames" do
    @app = build_app(".example.com")

    get "/", env: {
      "HOST" => "attacker.com#x.example.com",
      "action_dispatch.show_detailed_exceptions" => true
    }

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

Copy link
Member Author

Choose a reason for hiding this comment

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

Both of these are not blocked currently.

Copy link
Member Author

Choose a reason for hiding this comment

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

I wonder of they can be taken advantage of 🤔 They are not valid hostnames (Wikipedia, see "LDH rule")

$ host example.com#example.org
Host example.com#example.org not found: 3(NXDOMAIN)

$ host .example.org
host: '.example.org' is not a legal name (empty label)

Copy link
Member Author

Choose a reason for hiding this comment

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

I handled them both in 9f0d130, no longer allowed.

I think there's one difference between Sinatra and Rails now: with .example.com in the list of permitted hosts, Rails will allow bar.example.com but not foo.bar.example.com. Sinatra will allow both. This is intentional, I think it makes sense. WDYT @walro?

Copy link
Contributor

@walro walro Nov 17, 2024

Choose a reason for hiding this comment

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

Both ways seem acceptable to me. I guess for Rails one would need to add .bar.example.com in addition to .example.com to allow for foo.bar.example.com?

In the Sinatra case, to only allow foo.bar.example.com (and its siblings) but not baz.example.com, one could setup .bar.example.com?

Copy link
Member Author

@dentarg dentarg Nov 17, 2024

Choose a reason for hiding this comment

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

Yes

Copy link
Member Author

Choose a reason for hiding this comment

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

Noticed an interesting comment in rack/rack#1604

Given RFC 2181 relaxes all restrictions on DNS names (except segment and total length), it seems unnecessarily risky to use anything but a wildcard match in that position.

Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ def assert_response(outcome:, headers:, last_response:)
good_requests = lambda do |allowed_host|
[
{ "HTTP_HOST" => allowed_host },
{ "HTTP_HOST" => "#{allowed_host}:3000" },
{ "HTTP_X_FORWARDED_HOST" => allowed_host },
{ "HTTP_X_FORWARDED_HOST" => "example.com, #{allowed_host}" },
{ "HTTP_FORWARDED" => "host=#{allowed_host}" },
Expand Down
Loading