1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167
|
From fd6a64fef1d0f7f40a8d4b046da882e83163299c Mon Sep 17 00:00:00 2001
From: Stef Schenkelaars <stef.schenkelaars@gmail.com>
Date: Wed, 7 Jul 2021 12:06:32 +0200
Subject: [PATCH] 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]
---
.../middleware/host_authorization.rb | 10 +--
.../test/dispatch/host_authorization_test.rb | 89 ++++++++++++++++++-
2 files changed, 91 insertions(+), 8 deletions(-)
Index: rails-6.0.3.7+dfsg/actionpack/lib/action_dispatch/middleware/host_authorization.rb
===================================================================
--- rails-6.0.3.7+dfsg.orig/actionpack/lib/action_dispatch/middleware/host_authorization.rb
+++ rails-6.0.3.7+dfsg/actionpack/lib/action_dispatch/middleware/host_authorization.rb
@@ -46,7 +46,7 @@ module ActionDispatch
def sanitize_string(host)
if host.start_with?(".")
- /\A(.+\.)?#{Regexp.escape(host[1..-1])}\z/
+ /\A([a-z0-9-]+\.)?#{Regexp.escape(host[1..-1])}\z/i
else
host
end
@@ -86,13 +86,9 @@ module ActionDispatch
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
Index: rails-6.0.3.7+dfsg/actionpack/test/dispatch/host_authorization_test.rb
===================================================================
--- rails-6.0.3.7+dfsg.orig/actionpack/test/dispatch/host_authorization_test.rb
+++ rails-6.0.3.7+dfsg/actionpack/test/dispatch/host_authorization_test.rb
@@ -111,6 +111,44 @@ class HostAuthorizationTest < ActionDisp
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)
@@ -147,11 +185,23 @@ class HostAuthorizationTest < ActionDisp
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",
}
@@ -169,4 +219,41 @@ class HostAuthorizationTest < ActionDisp
assert_response :forbidden
assert_match "Blocked host: attacker.com#x.example.com", response.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
end
|