# frozen_string_literal: true require "abstract_unit" require "ipaddr" class HostAuthorizationTest < ActionDispatch::IntegrationTest App = -> env { [200, {}, %w(Success)] } test "blocks requests to unallowed host" do @app = ActionDispatch::HostAuthorization.new(App, %w(only.com)) get "/" assert_response :forbidden assert_match "Blocked host: www.example.com", response.body end test "allows all requests if hosts is empty" do @app = ActionDispatch::HostAuthorization.new(App, nil) get "/" assert_response :ok assert_equal "Success", body end test "hosts can be a single element array" do @app = ActionDispatch::HostAuthorization.new(App, %w(www.example.com)) get "/" assert_response :ok assert_equal "Success", body end test "hosts can be a string" do @app = ActionDispatch::HostAuthorization.new(App, "www.example.com") get "/" assert_response :ok assert_equal "Success", body end test "hosts are matched case insensitive" do @app = ActionDispatch::HostAuthorization.new(App, "Example.local") get "/", env: { "HOST" => "example.local", } assert_response :ok assert_equal "Success", body end test "hosts are matched case insensitive with titlecased host" do @app = ActionDispatch::HostAuthorization.new(App, "example.local") get "/", env: { "HOST" => "Example.local", } assert_response :ok assert_equal "Success", body end test "hosts are matched case insensitive with hosts array" do @app = ActionDispatch::HostAuthorization.new(App, ["Example.local"]) get "/", env: { "HOST" => "example.local", } assert_response :ok assert_equal "Success", body end test "regex matches are not title cased" do @app = ActionDispatch::HostAuthorization.new(App, [/www.Example.local/]) get "/", env: { "HOST" => "www.example.local", } assert_response :forbidden assert_match "Blocked host: www.example.local", response.body end test "passes requests to allowed hosts with domain name notation" do @app = ActionDispatch::HostAuthorization.new(App, ".example.com") get "/" assert_response :ok assert_equal "Success", body end test "does not allow domain name notation in the HOST header itself" do @app = ActionDispatch::HostAuthorization.new(App, ".example.com") get "/", env: { "HOST" => ".example.com", } assert_response :forbidden assert_match "Blocked host: .example.com", response.body end test "checks for requests with #=== to support wider range of host checks" do @app = ActionDispatch::HostAuthorization.new(App, [-> input { input == "www.example.com" }]) get "/" assert_response :ok assert_equal "Success", body end test "mark the host when authorized" do @app = ActionDispatch::HostAuthorization.new(App, ".example.com") get "/" assert_equal "www.example.com", request.get_header("action_dispatch.authorized_host") end test "sanitizes regular expressions to prevent accidental matches" do @app = ActionDispatch::HostAuthorization.new(App, [/w.example.co/]) get "/" assert_response :forbidden assert_match "Blocked host: www.example.com", response.body end test "blocks requests to unallowed host supporting custom responses" do @app = ActionDispatch::HostAuthorization.new(App, ["w.example.co"], response_app: -> env do [401, {}, %w(Custom)] end) get "/" assert_response :unauthorized assert_equal "Custom", body end test "blocks requests with spoofed X-FORWARDED-HOST" do @app = ActionDispatch::HostAuthorization.new(App, [IPAddr.new("127.0.0.1")]) get "/", env: { "HTTP_X_FORWARDED_HOST" => "127.0.0.1", "HOST" => "www.example.com", } assert_response :forbidden assert_match "Blocked host: 127.0.0.1", response.body end test "does not consider IP addresses in X-FORWARDED-HOST spoofed when disabled" do @app = ActionDispatch::HostAuthorization.new(App, nil) get "/", env: { "HTTP_X_FORWARDED_HOST" => "127.0.0.1", "HOST" => "www.example.com", } assert_response :ok assert_equal "Success", body end test "detects localhost domain spoofing" do @app = ActionDispatch::HostAuthorization.new(App, "localhost") get "/", env: { "HTTP_X_FORWARDED_HOST" => "localhost", "HOST" => "www.example.com", } assert_response :forbidden assert_match "Blocked host: localhost", response.body end test "forwarded hosts should be permitted" do @app = ActionDispatch::HostAuthorization.new(App, "domain.com") get "/", env: { "HTTP_X_FORWARDED_HOST" => "sub.domain.com", "HOST" => "domain.com", } assert_response :forbidden assert_match "Blocked host: 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", "HOST" => "domain.com", } assert_response :ok assert_equal "Success", body end test "exclude matches allow any host" do @app = ActionDispatch::HostAuthorization.new(App, "only.com", exclude: ->(req) { req.path == "/foo" }) get "/foo" assert_response :ok assert_equal "Success", body end test "exclude misses block unallowed hosts" do @app = ActionDispatch::HostAuthorization.new(App, "only.com", exclude: ->(req) { req.path == "/bar" }) get "/foo" assert_response :forbidden assert_match "Blocked host: www.example.com", response.body end test "blocks requests with invalid hostnames" do @app = ActionDispatch::HostAuthorization.new(App, ".example.com") get "/", env: { "HOST" => "attacker.com#x.example.com", } assert_response :forbidden assert_match "Blocked host: attacker.com#x.example.com", response.body end test "blocks requests to similar host" do @app = ActionDispatch::HostAuthorization.new(App, "sub.example.com") get "/", env: { "HOST" => "sub-example.com", } assert_response :forbidden assert_match "Blocked host: sub-example.com", response.body end test "config setting action_dispatch.hosts_response_app is deprecated" do assert_deprecated do ActionDispatch::HostAuthorization.new(App, "example.com", ->(env) { true }) end end end