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
|
From 9fe57c0fc5561088a2df42e4438992591e9d917e Mon Sep 17 00:00:00 2001
From: Jonathan Hefner <jonathan@hefner.pro>
Date: Fri, 12 Feb 2021 12:59:54 -0600
Subject: [PATCH] Refactor CVE-2021-22881 fix
Follow-up to 83a6ac3fee8fd538ce7e0088913ff54f0f9bcb6f.
This allows `HTTP_HOST` to be omitted as before, and reduces the number
of object allocations per request.
Benchmark:
```ruby
# frozen_string_literal: true
require "benchmark/memory"
HOST = "example.com:80"
BEFORE_REGEXP = /\A(?<host>[a-z0-9.-]+|\[[a-f0-9]*:[a-f0-9.:]+\])(:\d+)?\z/
AFTER_REGEXP = /(?:\A|,[ ]?)([a-z0-9.-]+|\[[a-f0-9]*:[a-f0-9.:]+\])(?::\d+)?\z/i
Benchmark.memory do |x|
x.report("BEFORE (non-nil X-Forwarded-Host)") do
origin_host = BEFORE_REGEXP.match(HOST.to_s.downcase)[:host]
forwarded_host = BEFORE_REGEXP.match(HOST.to_s.split(/,\s?/).last)[:host]
end
x.report("BEFORE (nil X-Forwarded-Host)") do
origin_host = BEFORE_REGEXP.match(HOST.to_s.downcase)[:host]
forwarded_host = BEFORE_REGEXP.match(nil.to_s.split(/,\s?/).last)
end
x.report("AFTER (non-nil X-Forwarded-Host)") do
origin_host = HOST&.slice(AFTER_REGEXP, 1) || ""
forwarded_host = HOST&.slice(AFTER_REGEXP, 1) || ""
end
x.report("AFTER (nil X-Forwarded-Host)") do
origin_host = HOST&.slice(AFTER_REGEXP, 1) || ""
forwarded_host = nil&.slice(AFTER_REGEXP, 1) || ""
end
end
```
Results:
```
BEFORE (non-nil X-Forwarded-Host)
616.000 memsize ( 208.000 retained)
9.000 objects ( 2.000 retained)
2.000 strings ( 1.000 retained)
BEFORE (nil X-Forwarded-Host)
328.000 memsize ( 0.000 retained)
5.000 objects ( 0.000 retained)
2.000 strings ( 0.000 retained)
AFTER (non-nil X-Forwarded-Host)
248.000 memsize ( 168.000 retained)
3.000 objects ( 1.000 retained)
1.000 strings ( 0.000 retained)
AFTER (nil X-Forwarded-Host)
40.000 memsize ( 0.000 retained)
1.000 objects ( 0.000 retained)
1.000 strings ( 0.000 retained)
```
[CVE-2021-22942]
---
.../middleware/host_authorization.rb | 22 +++++++------------
.../test/dispatch/host_authorization_test.rb | 4 ++--
.../application/middleware/remote_ip_test.rb | 1 -
railties/test/isolation/abstract_unit.rb | 2 +-
4 files changed, 11 insertions(+), 18 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
@@ -86,21 +86,15 @@ module ActionDispatch
end
private
- def authorized?(request)
- valid_host = /
- \A
- (?<host>[a-z0-9.-]+|\[[a-f0-9]*:[a-f0-9.:]+\])
- (:\d+)?
- \z
- /x
+ 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/
- origin_host = valid_host.match(
- request.get_header("HTTP_HOST").to_s.downcase)
- forwarded_host = valid_host.match(
- request.x_forwarded_host.to_s.split(/,\s?/).last)
+ 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 && @permissions.allows?(origin_host[:host]) && (
- forwarded_host.nil? || @permissions.allows?(forwarded_host[:host]))
+ @permissions.allows?(origin_host) && (forwarded_host.blank? || @permissions.allows?(forwarded_host))
end
def mark_as_authorized(request)
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
@@ -163,10 +163,10 @@ class HostAuthorizationTest < ActionDisp
@app = ActionDispatch::HostAuthorization.new(App, ".example.com")
get "/", env: {
- "HOST" => "example.com#sub.example.com",
+ "HOST" => "attacker.com#x.example.com",
}
assert_response :forbidden
- assert_match "Blocked host: example.com#sub.example.com", response.body
+ assert_match "Blocked host: attacker.com#x.example.com", response.body
end
end
Index: rails-6.0.3.7+dfsg/railties/test/application/middleware/remote_ip_test.rb
===================================================================
--- rails-6.0.3.7+dfsg.orig/railties/test/application/middleware/remote_ip_test.rb
+++ rails-6.0.3.7+dfsg/railties/test/application/middleware/remote_ip_test.rb
@@ -11,7 +11,6 @@ module ApplicationTests
def remote_ip(env = {})
remote_ip = nil
env = Rack::MockRequest.env_for("/").merge(env).merge!(
- "HTTP_HOST" => "example.com",
"action_dispatch.show_exceptions" => false,
"action_dispatch.key_generator" => ActiveSupport::CachingKeyGenerator.new(
ActiveSupport::KeyGenerator.new("b3c631c314c0bbca50c1b2843150fe33", iterations: 1000)
Index: rails-6.0.3.7+dfsg/railties/test/isolation/abstract_unit.rb
===================================================================
--- rails-6.0.3.7+dfsg.orig/railties/test/isolation/abstract_unit.rb
+++ rails-6.0.3.7+dfsg/railties/test/isolation/abstract_unit.rb
@@ -77,7 +77,7 @@ module TestHelpers
end
def get(path)
- @app.call(::Rack::MockRequest.env_for(path, "HTTP_HOST" => "example.com"))
+ @app.call(::Rack::MockRequest.env_for(path))
end
def assert_welcome(resp)
|