Add Rack::Lint to HostAuthorization tests

This adds additional test coverage to HostAuthorization to validate that
its behavior conforms to the Rack SPEC.

By using Rack:: constants for Content-Type and Content-Length, we are
able to use the "correct" versions of the headers for applications using
each Rack version.

Additionally, two tests had to be updated that use an ipv6 address
without brackets in the HOST header because Rack::Lint warned that these
addresses were not valid HOST values. Rack::Lint checks HOST headers using
`URI.parse("http://#{HOST}/")`, and from what I could find, this
requirement follows RFC 3986 Section 3.2.2:

```
host        = IP-literal / IPv4address / reg-name
IP-literal = "[" ( IPv6address / IPvFuture  ) "]"
IPvFuture  = "v" 1*HEXDIG "." 1*( unreserved / sub-delims / ":" )
```
This commit is contained in:
Hartley McGuire 2023-07-26 09:50:06 -04:00
parent 3bdd57fba6
commit 37522f1596
No known key found for this signature in database
GPG Key ID: E823FC1403858A82
2 changed files with 54 additions and 45 deletions

View File

@ -104,8 +104,8 @@ module ActionDispatch
def response(format, body)
[RESPONSE_STATUS,
{ "Content-Type" => "#{format}; charset=#{Response.default_charset}",
"Content-Length" => body.bytesize.to_s },
{ Rack::CONTENT_TYPE => "#{format}; charset=#{Response.default_charset}",
Rack::CONTENT_LENGTH => body.bytesize.to_s },
[body]]
end

View File

@ -7,7 +7,7 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
App = -> env { [200, {}, %w(Success)] }
test "blocks requests to unallowed host with empty body" do
@app = ActionDispatch::HostAuthorization.new(App, %w(only.com))
@app = build_app(%w(only.com))
get "/"
@ -16,7 +16,7 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
end
test "renders debug info when all requests considered as local" do
@app = ActionDispatch::HostAuthorization.new(App, %w(only.com))
@app = build_app(%w(only.com))
get "/", env: { "action_dispatch.show_detailed_exceptions" => true }
@ -25,7 +25,7 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
end
test "allows all requests if hosts is empty" do
@app = ActionDispatch::HostAuthorization.new(App, nil)
@app = build_app(nil)
get "/"
@ -34,7 +34,7 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
end
test "hosts can be a single element array" do
@app = ActionDispatch::HostAuthorization.new(App, %w(www.example.com))
@app = build_app(%w(www.example.com))
get "/"
@ -43,7 +43,7 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
end
test "hosts can be a string" do
@app = ActionDispatch::HostAuthorization.new(App, "www.example.com")
@app = build_app("www.example.com")
get "/"
@ -52,7 +52,7 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
end
test "hosts are matched case insensitive" do
@app = ActionDispatch::HostAuthorization.new(App, "Example.local")
@app = build_app("Example.local")
get "/", env: {
"HOST" => "example.local",
@ -63,7 +63,7 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
end
test "hosts are matched case insensitive with titlecased host" do
@app = ActionDispatch::HostAuthorization.new(App, "example.local")
@app = build_app("example.local")
get "/", env: {
"HOST" => "Example.local",
@ -74,7 +74,7 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
end
test "hosts are matched case insensitive with hosts array" do
@app = ActionDispatch::HostAuthorization.new(App, ["Example.local"])
@app = build_app(["Example.local"])
get "/", env: {
"HOST" => "example.local",
@ -85,7 +85,7 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
end
test "regex matches are not title cased" do
@app = ActionDispatch::HostAuthorization.new(App, [/www.Example.local/])
@app = build_app([/www.Example.local/])
get "/", env: {
"HOST" => "www.example.local",
@ -97,7 +97,7 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
end
test "passes requests to allowed hosts with domain name notation" do
@app = ActionDispatch::HostAuthorization.new(App, ".example.com")
@app = build_app(".example.com")
get "/"
@ -106,7 +106,7 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
end
test "does not allow domain name notation in the HOST header itself" do
@app = ActionDispatch::HostAuthorization.new(App, ".example.com")
@app = build_app(".example.com")
get "/", env: {
"HOST" => ".example.com",
@ -118,7 +118,7 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
end
test "checks for requests with #=== to support wider range of host checks" do
@app = ActionDispatch::HostAuthorization.new(App, [-> input { input == "www.example.com" }])
@app = build_app([-> input { input == "www.example.com" }])
get "/"
@ -127,7 +127,7 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
end
test "mark the host when authorized" do
@app = ActionDispatch::HostAuthorization.new(App, ".example.com")
@app = build_app(".example.com")
get "/"
@ -135,7 +135,7 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
end
test "sanitizes regular expressions to prevent accidental matches" do
@app = ActionDispatch::HostAuthorization.new(App, [/w.example.co/])
@app = build_app([/w.example.co/])
get "/", env: { "action_dispatch.show_detailed_exceptions" => true }
@ -144,7 +144,7 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
end
test "blocks requests to unallowed host supporting custom responses" do
@app = ActionDispatch::HostAuthorization.new(App, ["w.example.co"], response_app: -> env do
@app = build_app(["w.example.co"], response_app: -> env do
[401, {}, %w(Custom)]
end)
@ -155,7 +155,7 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
end
test "localhost works in dev" do
@app = ActionDispatch::HostAuthorization.new(App, ActionDispatch::HostAuthorization::ALLOWED_HOSTS_IN_DEVELOPMENT)
@app = build_app(ActionDispatch::HostAuthorization::ALLOWED_HOSTS_IN_DEVELOPMENT)
get "/", env: {
"HOST" => "localhost:3000",
@ -167,7 +167,7 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
end
test "localhost using IPV4 works in dev" do
@app = ActionDispatch::HostAuthorization.new(App, ActionDispatch::HostAuthorization::ALLOWED_HOSTS_IN_DEVELOPMENT)
@app = build_app(ActionDispatch::HostAuthorization::ALLOWED_HOSTS_IN_DEVELOPMENT)
get "/", env: {
"HOST" => "127.0.0.1",
@ -179,7 +179,7 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
end
test "localhost using IPV4 with port works in dev" do
@app = ActionDispatch::HostAuthorization.new(App, ActionDispatch::HostAuthorization::ALLOWED_HOSTS_IN_DEVELOPMENT)
@app = build_app(ActionDispatch::HostAuthorization::ALLOWED_HOSTS_IN_DEVELOPMENT)
get "/", env: {
"HOST" => "127.0.0.1:3000",
@ -191,7 +191,7 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
end
test "localhost using IPV4 binding in all addresses works in dev" do
@app = ActionDispatch::HostAuthorization.new(App, ActionDispatch::HostAuthorization::ALLOWED_HOSTS_IN_DEVELOPMENT)
@app = build_app(ActionDispatch::HostAuthorization::ALLOWED_HOSTS_IN_DEVELOPMENT)
get "/", env: {
"HOST" => "0.0.0.0",
@ -203,7 +203,7 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
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)
@app = build_app(ActionDispatch::HostAuthorization::ALLOWED_HOSTS_IN_DEVELOPMENT)
get "/", env: {
"HOST" => "0.0.0.0:3000",
@ -215,10 +215,10 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
end
test "localhost using IPV6 works in dev" do
@app = ActionDispatch::HostAuthorization.new(App, ActionDispatch::HostAuthorization::ALLOWED_HOSTS_IN_DEVELOPMENT)
@app = build_app(ActionDispatch::HostAuthorization::ALLOWED_HOSTS_IN_DEVELOPMENT)
get "/", env: {
"HOST" => "::1",
"HOST" => "[::1]",
"action_dispatch.show_detailed_exceptions" => true
}
@ -227,7 +227,7 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
end
test "localhost using IPV6 with port works in dev" do
@app = ActionDispatch::HostAuthorization.new(App, ActionDispatch::HostAuthorization::ALLOWED_HOSTS_IN_DEVELOPMENT)
@app = build_app(ActionDispatch::HostAuthorization::ALLOWED_HOSTS_IN_DEVELOPMENT)
get "/", env: {
"HOST" => "[::1]:3000",
@ -239,10 +239,10 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
end
test "localhost using IPV6 binding in all addresses works in dev" do
@app = ActionDispatch::HostAuthorization.new(App, ActionDispatch::HostAuthorization::ALLOWED_HOSTS_IN_DEVELOPMENT)
@app = build_app(ActionDispatch::HostAuthorization::ALLOWED_HOSTS_IN_DEVELOPMENT)
get "/", env: {
"HOST" => "::",
"HOST" => "[::]",
"action_dispatch.show_detailed_exceptions" => true
}
@ -251,7 +251,7 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
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)
@app = build_app(ActionDispatch::HostAuthorization::ALLOWED_HOSTS_IN_DEVELOPMENT)
get "/", env: {
"HOST" => "[::]:3000",
@ -263,7 +263,7 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
end
test "hosts with port works" do
@app = ActionDispatch::HostAuthorization.new(App, ["host.test"])
@app = build_app(["host.test"])
get "/", env: {
"HOST" => "host.test:3000",
@ -275,7 +275,7 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
end
test "blocks requests with spoofed X-FORWARDED-HOST" do
@app = ActionDispatch::HostAuthorization.new(App, [IPAddr.new("127.0.0.1")])
@app = build_app([IPAddr.new("127.0.0.1")])
get "/", env: {
"HTTP_X_FORWARDED_HOST" => "127.0.0.1",
@ -288,7 +288,7 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
end
test "blocks requests with spoofed relative X-FORWARDED-HOST" do
@app = ActionDispatch::HostAuthorization.new(App, ["www.example.com"])
@app = build_app(["www.example.com"])
get "/", env: {
"HTTP_X_FORWARDED_HOST" => "//randomhost.com",
@ -301,7 +301,7 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
end
test "forwarded secondary hosts are allowed when permitted" do
@app = ActionDispatch::HostAuthorization.new(App, ".domain.com")
@app = build_app(".domain.com")
get "/", env: {
"HTTP_X_FORWARDED_HOST" => "example.com, my-sub.domain.com",
@ -313,7 +313,7 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
end
test "forwarded secondary hosts are blocked when mismatch" do
@app = ActionDispatch::HostAuthorization.new(App, "domain.com")
@app = build_app("domain.com")
get "/", env: {
"HTTP_X_FORWARDED_HOST" => "domain.com, evil.com",
@ -326,7 +326,7 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
end
test "does not consider IP addresses in X-FORWARDED-HOST spoofed when disabled" do
@app = ActionDispatch::HostAuthorization.new(App, nil)
@app = build_app(nil)
get "/", env: {
"HTTP_X_FORWARDED_HOST" => "127.0.0.1",
@ -338,7 +338,7 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
end
test "detects localhost domain spoofing" do
@app = ActionDispatch::HostAuthorization.new(App, "localhost")
@app = build_app("localhost")
get "/", env: {
"HTTP_X_FORWARDED_HOST" => "localhost",
@ -351,7 +351,7 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
end
test "forwarded hosts should be permitted" do
@app = ActionDispatch::HostAuthorization.new(App, "domain.com")
@app = build_app("domain.com")
get "/", env: {
"HTTP_X_FORWARDED_HOST" => "sub.domain.com",
@ -364,7 +364,7 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
end
test "sub-sub domains should not be permitted" do
@app = ActionDispatch::HostAuthorization.new(App, ".domain.com")
@app = build_app(".domain.com")
get "/", env: {
"HOST" => "secondary.sub.domain.com",
@ -376,7 +376,7 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
end
test "forwarded hosts are allowed when permitted" do
@app = ActionDispatch::HostAuthorization.new(App, ".domain.com")
@app = build_app(".domain.com")
get "/", env: {
"HTTP_X_FORWARDED_HOST" => "my-sub.domain.com",
@ -410,7 +410,7 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
"hacker.com/"
]
@app = ActionDispatch::HostAuthorization.new(App, "example.com")
@app = build_app("example.com")
ng_hosts.each do |host|
get "/", env: {
@ -425,7 +425,7 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
end
test "exclude matches allow any host" do
@app = ActionDispatch::HostAuthorization.new(App, "only.com", exclude: ->(req) { req.path == "/foo" })
@app = build_app("only.com", exclude: ->(req) { req.path == "/foo" })
get "/foo"
@ -434,7 +434,7 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
end
test "exclude misses block unallowed hosts" do
@app = ActionDispatch::HostAuthorization.new(App, "only.com", exclude: ->(req) { req.path == "/bar" })
@app = build_app("only.com", exclude: ->(req) { req.path == "/bar" })
get "/foo", env: { "action_dispatch.show_detailed_exceptions" => true }
@ -443,7 +443,7 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
end
test "blocks requests with invalid hostnames" do
@app = ActionDispatch::HostAuthorization.new(App, ".example.com")
@app = build_app(".example.com")
get "/", env: {
"HOST" => "attacker.com#x.example.com",
@ -455,7 +455,7 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
end
test "blocks requests to similar host" do
@app = ActionDispatch::HostAuthorization.new(App, "sub.example.com")
@app = build_app("sub.example.com")
get "/", env: {
"HOST" => "sub-example.com",
@ -467,7 +467,7 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
end
test "uses logger from the env" do
@app = ActionDispatch::HostAuthorization.new(App, %w(only.com))
@app = build_app(%w(only.com))
output = StringIO.new
get "/", env: { "action_dispatch.logger" => Logger.new(output) }
@ -477,7 +477,7 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
end
test "uses ActionView::Base logger when no logger in the env" do
@app = ActionDispatch::HostAuthorization.new(App, %w(only.com))
@app = build_app(%w(only.com))
output = StringIO.new
logger = Logger.new(output)
@ -491,4 +491,13 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
assert_response :forbidden
assert_match "Blocked host: www.example.com", output.rewind && output.read
end
private
def build_app(hosts, exclude: nil, response_app: nil)
Rack::Lint.new(
ActionDispatch::HostAuthorization.new(
Rack::Lint.new(App), hosts, exclude: exclude, response_app: response_app
)
)
end
end