mirror of https://github.com/rails/rails
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:
parent
3bdd57fba6
commit
37522f1596
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue