CVE-2021-44528 (published 2022-01-10T14:10Z)

A open redirect vulnerability exists in Action Pack >= 6.0.0 that could allow an attacker to craft a "X-Forwarded-Host" headers in combination with certain "allowed host" formats can cause the Host Authorization middleware in Action Pack to redirect users to a malicious website.

Severity: MEDIUM
CVSS v2 base score: 5.8 (AV:N/AC:M/Au:N/C:P/I:P/A:N)
CVSS v3 base score: 6.1, MEDIUM (CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N)

Weakness information:
cwe_id: CWE-601
cwe_name: URL Redirection to Untrusted Site ('Open Redirect')
description: A web application accepts a user-controlled input that specifies a link to an external site, and uses that link in a Redirect. This simplifies phishing attacks.
extended_description: An http parameter may contain a URL value and could cause the web application to redirect the request to the specified URL. By modifying the URL value to a malicious site, an attacker may successfully launch a phishing scam and steal user credentials. Because the server name in the modified link is identical to the original site, phishing attempts have a more trustworthy appearance. Whether this issue poses a vulnerability will be subject to the intended behavior of the application. For example, a search engine might intentionally provide redirects to arbitrary URLs.
         

Fix information:
repo_url: https://github.com/rails/rails
commit_details: repo_url: https://github.com/rails/rails
author: Stef Schenkelaars
author_date: 2021-07-07 12:06:32+02:00
msg: Fix invalid forwarded host vulnerability

Prior to this commit, it was possible to pass an unvalidated host
through the `X-Forwarded-Host` header. If the value of the header
was prefixed with a invalid domain character (for example a `/`),
it was always accepted as the actual host of that request.

Since this host is used for all url helpers, an attacker could change
generated links and redirects. If the header is set to
`X-Forwarded-Host: //evil.hacker`, a redirect will be send to
`https:////evil.hacker/`. Browsers will ignore these four slashes
and redirect the user.

[CVE-2021-44528]
repository: repo_url: https://github.com/rails/rails
repo_name: rails/rails
description: Ruby on Rails
repo_language: Ruby
owner: rails
file_changes: filename: host_authorization.rb
change_type: ModificationType.MODIFY
diff: @@ -52,7 +52,7 @@ def sanitize_regexp(host)
 
         def sanitize_string(host)
           if host.start_with?(".")
-            /\A(.+\.)?#{Regexp.escape(host[1..-1])}\z/i
+            /\A([a-z0-9-]+\.)?#{Regexp.escape(host[1..-1])}\z/i
           else
             /\A#{Regexp.escape host}\z/i
           end
@@ -120,13 +120,9 @@ def call(env)
     end
 
     private
-      HOSTNAME = /[a-z0-9.-]+|\[[a-f0-9]*:[a-f0-9.:]+\]/i
-      VALID_ORIGIN_HOST = /\A(#{HOSTNAME})(?::\d+)?\z/
-      VALID_FORWARDED_HOST = /(?:\A|,[ ]?)(#{HOSTNAME})(?::\d+)?\z/
-
       def authorized?(request)
-        origin_host = request.get_header("HTTP_HOST")&.slice(VALID_ORIGIN_HOST, 1) || ""
-        forwarded_host = request.x_forwarded_host&.slice(VALID_FORWARDED_HOST, 1) || ""
+        origin_host = request.get_header("HTTP_HOST")
+        forwarded_host = request.x_forwarded_host&.split(/,\s?/)&.last
 
         @permissions.allows?(origin_host) && (forwarded_host.blank? || @permissions.allows?(forwarded_host))
       end

programming_language: Ruby
filename: host_authorization_test.rb
change_type: ModificationType.MODIFY
diff: @@ -167,6 +167,44 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
     assert_match "Blocked host: 127.0.0.1", response.body
   end
 
+  test "blocks requests with spoofed relative X-FORWARDED-HOST" do
+    @app = ActionDispatch::HostAuthorization.new(App, ["www.example.com"])
+
+    get "/", env: {
+      "HTTP_X_FORWARDED_HOST" => "//randomhost.com",
+      "HOST" => "www.example.com",
+      "action_dispatch.show_detailed_exceptions" => true
+    }
+
+    assert_response :forbidden
+    assert_match "Blocked host: //randomhost.com", response.body
+  end
+
+  test "forwarded secondary hosts are allowed when permitted" do
+    @app = ActionDispatch::HostAuthorization.new(App, ".domain.com")
+
+    get "/", env: {
+      "HTTP_X_FORWARDED_HOST" => "example.com, my-sub.domain.com",
+      "HOST" => "domain.com",
+    }
+
+    assert_response :ok
+    assert_equal "Success", body
+  end
+
+  test "forwarded secondary hosts are blocked when mismatch" do
+    @app = ActionDispatch::HostAuthorization.new(App, "domain.com")
+
+    get "/", env: {
+      "HTTP_X_FORWARDED_HOST" => "domain.com, evil.com",
+      "HOST" => "domain.com",
+      "action_dispatch.show_detailed_exceptions" => true
+    }
+
+    assert_response :forbidden
+    assert_match "Blocked host: evil.com", response.body
+  end
+
   test "does not consider IP addresses in X-FORWARDED-HOST spoofed when disabled" do
     @app = ActionDispatch::HostAuthorization.new(App, nil)
 
@@ -205,11 +243,23 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
     assert_match "Blocked host: sub.domain.com", response.body
   end
 
+  test "sub-sub domains should not be permitted" do
+    @app = ActionDispatch::HostAuthorization.new(App, ".domain.com")
+
+    get "/", env: {
+      "HOST" => "secondary.sub.domain.com",
+      "action_dispatch.show_detailed_exceptions" => true
+    }
+
+    assert_response :forbidden
+    assert_match "Blocked host: secondary.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",
+      "HTTP_X_FORWARDED_HOST" => "my-sub.domain.com",
       "HOST" => "domain.com",
     }
 
@@ -217,6 +267,43 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
     assert_equal "Success", body
   end
 
+  test "lots of NG hosts" do
+    ng_hosts = [
+      "hacker%E3%80%82com",
+      "hacker%00.com",
+      "www.theirsite.com@yoursite.com",
+      "hacker.com/test/",
+      "hacker%252ecom",
+      ".hacker.com",
+      "/\/\/hacker.com/",
+      "/hacker.com",
+      "../hacker.com",
+      ".hacker.com",
+      "@hacker.com",
+      "hacker.com",
+      "hacker.com%23@example.com",
+      "hacker.com/.jpg",
+      "hacker.com\texample.com/",
+      "hacker.com/example.com",
+      "hacker.com\@example.com",
+      "hacker.com/example.com",
+      "hacker.com/"
+    ]
+
+    @app = ActionDispatch::HostAuthorization.new(App, "example.com")
+
+    ng_hosts.each do |host|
+      get "/", env: {
+        "HTTP_X_FORWARDED_HOST" => host,
+        "HOST" => "example.com",
+        "action_dispatch.show_detailed_exceptions" => true
+      }
+
+      assert_response :forbidden
+      assert_match "Blocked host: #{host}", response.body
+    end
+  end
+
   test "exclude matches allow any host" do
     @app = ActionDispatch::HostAuthorization.new(App, "only.com", exclude: ->(req) { req.path == "/foo" })
 

programming_language: Ruby