CVE-2021-44528
Description
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.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Open redirect in Rails Action Pack Host Authorization middleware allows attackers to redirect users to malicious sites via crafted X-Forwarded-Host header.
Vulnerability
The vulnerability is an open redirect in the Host Authorization middleware of Action Pack (part of Ruby on Rails) affecting versions >= 6.0.0. It allows an attacker to craft a X-Forwarded-Host header that, in combination with certain formats of allowed host configurations (e.g., using wildcards or specific patterns), can bypass host validation and redirect users to an arbitrary external domain. [1][2]
Exploitation
An attacker must be able to send HTTP requests to the target application with a specially crafted X-Forwarded-Host header. The application must be using Action Pack's Host Authorization middleware (common in production) and have a permissive allowed host configuration, such as a wildcard pattern. The attacker can then trigger a redirect to a malicious site by exploiting the middleware's handling of the forwarded host. [1][4]
Impact
Successful exploitation allows an attacker to redirect users from the legitimate application to an arbitrary external website, enabling phishing attacks or other social engineering schemes. The redirect is performed by the application itself, so users may trust the destination. This is a confidentiality and integrity impact, as user trust and data could be compromised. [2]
Mitigation
The vulnerability is fixed in Rails versions 6.1.4.2 and 6.0.4.2 (patched releases). Users should upgrade to these versions or later. If immediate upgrade is not possible, ensure that the allowed_hosts configuration is restrictive and avoid using wildcard patterns that could be exploited. [1][4]
AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
actionpackRubyGems | >= 6.0.0, < 6.0.4.2 | 6.0.4.2 |
actionpackRubyGems | >= 6.1.0, < 6.1.4.2 | 6.1.4.2 |
Affected products
4- Action Pack/Action Packdescription
- osv-coords3 versionspkg:bitnami/railspkg:gem/actionpackpkg:rpm/opensuse/rubygem-actionpack-6.0&distro=openSUSE%20Tumbleweed
>= 7.0.0-rc2, <= 7.0.0-rc2+ 2 more
- (no CPE)range: >= 7.0.0-rc2, <= 7.0.0-rc2
- (no CPE)range: >= 6.0.0, < 6.0.4.2
- (no CPE)range: < 6.0.4.4-1.1
Patches
20fccfb9a3097Fix invalid forwarded host vulnerability
2 files changed · +91 −8
actionpack/lib/action_dispatch/middleware/host_authorization.rb+3 −7 modified@@ -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
actionpack/test/dispatch/host_authorization_test.rb+88 −1 modified@@ -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,18 +243,67 @@ 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", } assert_response :ok 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" })
aecba3c301b8Fix invalid forwarded host vulnerability
2 files changed · +91 −8
actionpack/lib/action_dispatch/middleware/host_authorization.rb+3 −7 modified@@ -51,7 +51,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 @@ -102,13 +102,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
actionpack/test/dispatch/host_authorization_test.rb+88 −1 modified@@ -155,6 +155,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) @@ -191,18 +229,67 @@ 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", } assert_response :ok 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" })
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
11- github.com/advisories/GHSA-qphc-hf5q-v8fcghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2021-44528ghsaADVISORY
- www.debian.org/security/2023/dsa-5372ghsavendor-advisoryWEB
- github.com/rails/rails/blob/v6.1.4.2/actionpack/CHANGELOG.mdghsaWEB
- github.com/rails/rails/commit/0fccfb9a3097a9c4260c791f1a40b128517e7815ghsaWEB
- github.com/rails/rails/commit/aecba3c301b80e9d5a63c30ea1b287bceaf2c107ghsaWEB
- github.com/rubysec/ruby-advisory-db/blob/master/gems/actionpack/CVE-2021-44528.ymlghsaWEB
- groups.google.com/g/ruby-security-ann/c/vG9gz3nk1pM/m/7-NU4MNrDAAJghsaWEB
- groups.google.com/g/ruby-security-ann/c/vG9gz3nk1pM/m/7-NU4MNrDAAJghsaWEB
- security.netapp.com/advisory/ntap-20240208-0003ghsaWEB
- security.netapp.com/advisory/ntap-20240208-0003/mitre
News mentions
0No linked articles in our index yet.