Skip to content

Commit 860f59b

Browse files
committed
Merge PR rails#42872
2 parents 8e86e87 + 0d837f4 commit 860f59b

File tree

3 files changed

+94
-14
lines changed

3 files changed

+94
-14
lines changed

actionpack/CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
* Update `HostAuthorization` middleware to render debug info only
2+
when `config.consider_all_requests_local` is set to true.
3+
4+
Also, blocked host info is always logged with level `error`.
5+
6+
Fixes #42813
7+
8+
*Nikita Vyrko*
9+
110
* Add Server-Timing middleware
211

312
Server-Timing specification defines how the server can communicate to browsers performance metrics

actionpack/lib/action_dispatch/middleware/host_authorization.rb

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@ module ActionDispatch
1111
#
1212
# When a request comes to an unauthorized host, the +response_app+
1313
# application will be executed and rendered. If no +response_app+ is given, a
14-
# default one will run, which responds with <tt>403 Forbidden</tt>.
14+
# default one will run.
15+
# The default response app logs blocked host info with level 'error' and
16+
# responds with <tt>403 Forbidden</tt>. The body of the response contains debug info
17+
# if +config.consider_all_requests_local+ is set to true, otherwise the body is empty.
1518
class HostAuthorization
1619
class Permissions # :nodoc:
1720
def initialize(hosts)
@@ -56,17 +59,43 @@ def sanitize_string(host)
5659
end
5760
end
5861

59-
DEFAULT_RESPONSE_APP = -> env do
60-
request = Request.new(env)
62+
class DefaultResponseApp # :nodoc:
63+
RESPONSE_STATUS = 403
64+
65+
def call(env)
66+
request = Request.new(env)
67+
format = request.xhr? ? "text/plain" : "text/html"
68+
69+
log_error(request)
70+
response(format, response_body(request))
71+
end
72+
73+
private
74+
def response_body(request)
75+
return "" unless request.get_header("action_dispatch.show_detailed_exceptions")
76+
77+
template = DebugView.new(host: request.host)
78+
template.render(template: "rescues/blocked_host", layout: "rescues/layout")
79+
end
80+
81+
def response(format, body)
82+
[RESPONSE_STATUS,
83+
{ "Content-Type" => "#{format}; charset=#{Response.default_charset}",
84+
"Content-Length" => body.bytesize.to_s },
85+
[body]]
86+
end
87+
88+
def log_error(request)
89+
logger = available_logger(request)
6190

62-
format = request.xhr? ? "text/plain" : "text/html"
63-
template = DebugView.new(host: request.host)
64-
body = template.render(template: "rescues/blocked_host", layout: "rescues/layout")
91+
return unless logger
6592

66-
[403, {
67-
"Content-Type" => "#{format}; charset=#{Response.default_charset}",
68-
"Content-Length" => body.bytesize.to_s,
69-
}, [body]]
93+
logger.error("[#{self.class.name}] Blocked host: #{request.host}")
94+
end
95+
96+
def available_logger(request)
97+
request.logger || ActionView::Base.logger
98+
end
7099
end
71100

72101
def initialize(app, hosts, deprecated_response_app = nil, exclude: nil, response_app: nil)
@@ -83,7 +112,7 @@ def initialize(app, hosts, deprecated_response_app = nil, exclude: nil, response
83112
response_app ||= deprecated_response_app
84113
end
85114

86-
@response_app = response_app || DEFAULT_RESPONSE_APP
115+
@response_app = response_app || DefaultResponseApp.new
87116
end
88117

89118
def call(env)

actionpack/test/dispatch/host_authorization_test.rb

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,20 @@
66
class HostAuthorizationTest < ActionDispatch::IntegrationTest
77
App = -> env { [200, {}, %w(Success)] }
88

9-
test "blocks requests to unallowed host" do
9+
test "blocks requests to unallowed host with empty body" do
1010
@app = ActionDispatch::HostAuthorization.new(App, %w(only.com))
1111

1212
get "/"
1313

14+
assert_response :forbidden
15+
assert_empty response.body
16+
end
17+
18+
test "renders debug info when all requests considered as local" do
19+
@app = ActionDispatch::HostAuthorization.new(App, %w(only.com))
20+
21+
get "/", env: { "action_dispatch.show_detailed_exceptions" => true }
22+
1423
assert_response :forbidden
1524
assert_match "Blocked host: www.example.com", response.body
1625
end
@@ -80,6 +89,7 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
8089

8190
get "/", env: {
8291
"HOST" => "www.example.local",
92+
"action_dispatch.show_detailed_exceptions" => true
8393
}
8494

8595
assert_response :forbidden
@@ -100,6 +110,7 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
100110

101111
get "/", env: {
102112
"HOST" => ".example.com",
113+
"action_dispatch.show_detailed_exceptions" => true
103114
}
104115

105116
assert_response :forbidden
@@ -126,7 +137,7 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
126137
test "sanitizes regular expressions to prevent accidental matches" do
127138
@app = ActionDispatch::HostAuthorization.new(App, [/w.example.co/])
128139

129-
get "/"
140+
get "/", env: { "action_dispatch.show_detailed_exceptions" => true }
130141

131142
assert_response :forbidden
132143
assert_match "Blocked host: www.example.com", response.body
@@ -149,6 +160,7 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
149160
get "/", env: {
150161
"HTTP_X_FORWARDED_HOST" => "127.0.0.1",
151162
"HOST" => "www.example.com",
163+
"action_dispatch.show_detailed_exceptions" => true
152164
}
153165

154166
assert_response :forbidden
@@ -173,6 +185,7 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
173185
get "/", env: {
174186
"HTTP_X_FORWARDED_HOST" => "localhost",
175187
"HOST" => "www.example.com",
188+
"action_dispatch.show_detailed_exceptions" => true
176189
}
177190

178191
assert_response :forbidden
@@ -185,6 +198,7 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
185198
get "/", env: {
186199
"HTTP_X_FORWARDED_HOST" => "sub.domain.com",
187200
"HOST" => "domain.com",
201+
"action_dispatch.show_detailed_exceptions" => true
188202
}
189203

190204
assert_response :forbidden
@@ -215,7 +229,7 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
215229
test "exclude misses block unallowed hosts" do
216230
@app = ActionDispatch::HostAuthorization.new(App, "only.com", exclude: ->(req) { req.path == "/bar" })
217231

218-
get "/foo"
232+
get "/foo", env: { "action_dispatch.show_detailed_exceptions" => true }
219233

220234
assert_response :forbidden
221235
assert_match "Blocked host: www.example.com", response.body
@@ -226,6 +240,7 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
226240

227241
get "/", env: {
228242
"HOST" => "attacker.com#x.example.com",
243+
"action_dispatch.show_detailed_exceptions" => true
229244
}
230245

231246
assert_response :forbidden
@@ -237,6 +252,7 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
237252

238253
get "/", env: {
239254
"HOST" => "sub-example.com",
255+
"action_dispatch.show_detailed_exceptions" => true
240256
}
241257

242258
assert_response :forbidden
@@ -248,4 +264,30 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
248264
ActionDispatch::HostAuthorization.new(App, "example.com", ->(env) { true })
249265
end
250266
end
267+
268+
test "uses logger from the env" do
269+
@app = ActionDispatch::HostAuthorization.new(App, %w(only.com))
270+
output = StringIO.new
271+
272+
get "/", env: { "action_dispatch.logger" => Logger.new(output) }
273+
274+
assert_response :forbidden
275+
assert_match "Blocked host: www.example.com", output.rewind && output.read
276+
end
277+
278+
test "uses ActionView::Base logger when no logger in the env" do
279+
@app = ActionDispatch::HostAuthorization.new(App, %w(only.com))
280+
output = StringIO.new
281+
logger = Logger.new(output)
282+
283+
_old, ActionView::Base.logger = ActionView::Base.logger, logger
284+
begin
285+
get "/"
286+
ensure
287+
ActionView::Base.logger = _old
288+
end
289+
290+
assert_response :forbidden
291+
assert_match "Blocked host: www.example.com", output.rewind && output.read
292+
end
251293
end

0 commit comments

Comments
 (0)