From 3cfcb1a5f515c799946bd858a1949ffefda90316 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Rafael=20Mendon=C3=A7a=20Fran=C3=A7a?=
 <rafael@rubyonrails.org>
Date: Wed, 15 Dec 2021 16:56:48 -0500
Subject: [PATCH] Merge pull request #43882 from rails/rm-allow-ip-with-port

Allow IPs with port in the HostAuthorization middleware
---
 .../middleware/host_authorization.rb          | 36 +++++--
 .../test/dispatch/host_authorization_test.rb  | 96 +++++++++++++++++++
 2 files changed, 123 insertions(+), 9 deletions(-)

diff --git a/actionpack/lib/action_dispatch/middleware/host_authorization.rb b/actionpack/lib/action_dispatch/middleware/host_authorization.rb
index e26e6ddc97..ca68b0241a 100644
--- a/actionpack/lib/action_dispatch/middleware/host_authorization.rb
+++ b/actionpack/lib/action_dispatch/middleware/host_authorization.rb
@@ -11,7 +11,15 @@ module ActionDispatch
   # default one will run, which responds with +403 Forbidden+.
   class HostAuthorization
     ALLOWED_HOSTS_IN_DEVELOPMENT = [".localhost", IPAddr.new("0.0.0.0/0"), IPAddr.new("::/0")]
-    PORT_REGEX = /(?::\d+)?/.freeze
+    PORT_REGEX = /(?::\d+)/ # :nodoc:
+    IPV4_HOSTNAME = /(?<host>\d+\.\d+\.\d+\.\d+)#{PORT_REGEX}?/ # :nodoc:
+    IPV6_HOSTNAME = /(?<host>[a-f0-9]*:[a-f0-9.:]+)/i # :nodoc:
+    IPV6_HOSTNAME_WITH_PORT = /\[#{IPV6_HOSTNAME}\]#{PORT_REGEX}/i # :nodoc:
+    VALID_IP_HOSTNAME = Regexp.union( # :nodoc:
+      /\A#{IPV4_HOSTNAME}\z/,
+      /\A#{IPV6_HOSTNAME}\z/,
+      /\A#{IPV6_HOSTNAME_WITH_PORT}\z/,
+    )
 
     class Permissions # :nodoc:
       def initialize(hosts)
@@ -24,11 +32,17 @@ def empty?
 
       def allows?(host)
         @hosts.any? do |allowed|
-          allowed === host
-        rescue
-          # IPAddr#=== raises an error if you give it a hostname instead of
-          # IP. Treat similar errors as blocked access.
-          false
+          if allowed.is_a?(IPAddr)
+            begin
+              allowed === extract_hostname(host)
+            rescue
+              # IPAddr#=== raises an error if you give it a hostname instead of
+              # IP. Treat similar errors as blocked access.
+              false
+            end
+          else
+            allowed === host
+          end
         end
       end
 
@@ -44,16 +58,20 @@ def sanitize_hosts(hosts)
         end
 
         def sanitize_regexp(host)
-          /\A#{host}#{PORT_REGEX}\z/
+          /\A#{host}#{PORT_REGEX}?\z/
         end
 
         def sanitize_string(host)
           if host.start_with?(".")
-            /\A([a-z0-9-]+\.)?#{Regexp.escape(host[1..-1])}#{PORT_REGEX}\z/i
+            /\A([a-z0-9-]+\.)?#{Regexp.escape(host[1..-1])}#{PORT_REGEX}?\z/i
           else
-            /\A#{Regexp.escape host}#{PORT_REGEX}\z/i
+            /\A#{Regexp.escape host}#{PORT_REGEX}?\z/i
           end
         end
+
+        def extract_hostname(host)
+          host.slice(VALID_IP_HOSTNAME, "host") || host
+        end
     end
 
     DEFAULT_RESPONSE_APP = -> env do
diff --git a/actionpack/test/dispatch/host_authorization_test.rb b/actionpack/test/dispatch/host_authorization_test.rb
index 0bacaeb1ea..57125078c7 100644
--- a/actionpack/test/dispatch/host_authorization_test.rb
+++ b/actionpack/test/dispatch/host_authorization_test.rb
@@ -155,6 +155,102 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
     assert_match "Success", response.body
   end
 
+  test "localhost using IPV4 works in dev" do
+    @app = ActionDispatch::HostAuthorization.new(App, ActionDispatch::HostAuthorization::ALLOWED_HOSTS_IN_DEVELOPMENT)
+
+    get "/", env: {
+      "HOST" => "127.0.0.1",
+      "action_dispatch.show_detailed_exceptions" => true
+    }
+
+    assert_response :ok
+    assert_match "Success", response.body
+  end
+
+  test "localhost using IPV4 with port works in dev" do
+    @app = ActionDispatch::HostAuthorization.new(App, ActionDispatch::HostAuthorization::ALLOWED_HOSTS_IN_DEVELOPMENT)
+
+    get "/", env: {
+      "HOST" => "127.0.0.1:3000",
+      "action_dispatch.show_detailed_exceptions" => true
+    }
+
+    assert_response :ok
+    assert_match "Success", response.body
+  end
+
+  test "localhost using IPV4 binding in all addresses works in dev" do
+    @app = ActionDispatch::HostAuthorization.new(App, ActionDispatch::HostAuthorization::ALLOWED_HOSTS_IN_DEVELOPMENT)
+
+    get "/", env: {
+      "HOST" => "0.0.0.0",
+      "action_dispatch.show_detailed_exceptions" => true
+    }
+
+    assert_response :ok
+    assert_match "Success", response.body
+  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)
+
+    get "/", env: {
+      "HOST" => "0.0.0.0:3000",
+      "action_dispatch.show_detailed_exceptions" => true
+    }
+
+    assert_response :ok
+    assert_match "Success", response.body
+  end
+
+  test "localhost using IPV6 works in dev" do
+    @app = ActionDispatch::HostAuthorization.new(App, ActionDispatch::HostAuthorization::ALLOWED_HOSTS_IN_DEVELOPMENT)
+
+    get "/", env: {
+      "HOST" => "::1",
+      "action_dispatch.show_detailed_exceptions" => true
+    }
+
+    assert_response :ok
+    assert_match "Success", response.body
+  end
+
+  test "localhost using IPV6 with port works in dev" do
+    @app = ActionDispatch::HostAuthorization.new(App, ActionDispatch::HostAuthorization::ALLOWED_HOSTS_IN_DEVELOPMENT)
+
+    get "/", env: {
+      "HOST" => "[::1]:3000",
+      "action_dispatch.show_detailed_exceptions" => true
+    }
+
+    assert_response :ok
+    assert_match "Success", response.body
+  end
+
+  test "localhost using IPV6 binding in all addresses works in dev" do
+    @app = ActionDispatch::HostAuthorization.new(App, ActionDispatch::HostAuthorization::ALLOWED_HOSTS_IN_DEVELOPMENT)
+
+    get "/", env: {
+      "HOST" => "::",
+      "action_dispatch.show_detailed_exceptions" => true
+    }
+
+    assert_response :ok
+    assert_match "Success", response.body
+  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)
+
+    get "/", env: {
+      "HOST" => "[::]:3000",
+      "action_dispatch.show_detailed_exceptions" => true
+    }
+
+    assert_response :ok
+    assert_match "Success", response.body
+  end
+
   test "hosts with port works" do
     @app = ActionDispatch::HostAuthorization.new(App, ["host.test"])
 
-- 
2.39.2

