actionpack 6.0.5.1 → 6.1.7.1
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of actionpack might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CHANGELOG.md +393 -253
- data/MIT-LICENSE +1 -2
- data/lib/abstract_controller/base.rb +35 -2
- data/lib/abstract_controller/callbacks.rb +2 -2
- data/lib/abstract_controller/collector.rb +4 -2
- data/lib/abstract_controller/helpers.rb +105 -90
- data/lib/abstract_controller/railties/routes_helpers.rb +17 -1
- data/lib/abstract_controller/rendering.rb +9 -9
- data/lib/abstract_controller/translation.rb +8 -2
- data/lib/abstract_controller.rb +1 -0
- data/lib/action_controller/api.rb +2 -2
- data/lib/action_controller/base.rb +4 -2
- data/lib/action_controller/caching.rb +0 -1
- data/lib/action_controller/log_subscriber.rb +3 -3
- data/lib/action_controller/metal/conditional_get.rb +11 -3
- data/lib/action_controller/metal/content_security_policy.rb +1 -1
- data/lib/action_controller/metal/cookies.rb +3 -1
- data/lib/action_controller/metal/data_streaming.rb +1 -1
- data/lib/action_controller/metal/etag_with_template_digest.rb +3 -5
- data/lib/action_controller/metal/exceptions.rb +33 -0
- data/lib/action_controller/metal/head.rb +7 -4
- data/lib/action_controller/metal/helpers.rb +11 -1
- data/lib/action_controller/metal/http_authentication.rb +5 -2
- data/lib/action_controller/metal/implicit_render.rb +1 -1
- data/lib/action_controller/metal/instrumentation.rb +11 -9
- data/lib/action_controller/metal/live.rb +10 -1
- data/lib/action_controller/metal/logging.rb +20 -0
- data/lib/action_controller/metal/mime_responds.rb +6 -2
- data/lib/action_controller/metal/parameter_encoding.rb +35 -4
- data/lib/action_controller/metal/params_wrapper.rb +14 -8
- data/lib/action_controller/metal/permissions_policy.rb +46 -0
- data/lib/action_controller/metal/redirecting.rb +1 -1
- data/lib/action_controller/metal/rendering.rb +6 -0
- data/lib/action_controller/metal/request_forgery_protection.rb +1 -1
- data/lib/action_controller/metal/rescue.rb +1 -1
- data/lib/action_controller/metal/strong_parameters.rb +104 -16
- data/lib/action_controller/metal.rb +2 -2
- data/lib/action_controller/renderer.rb +23 -13
- data/lib/action_controller/test_case.rb +65 -56
- data/lib/action_controller.rb +2 -3
- data/lib/action_dispatch/http/cache.rb +18 -17
- data/lib/action_dispatch/http/content_security_policy.rb +6 -1
- data/lib/action_dispatch/http/filter_parameters.rb +1 -1
- data/lib/action_dispatch/http/filter_redirect.rb +1 -1
- data/lib/action_dispatch/http/headers.rb +3 -2
- data/lib/action_dispatch/http/mime_negotiation.rb +14 -8
- data/lib/action_dispatch/http/mime_type.rb +29 -16
- data/lib/action_dispatch/http/parameters.rb +1 -19
- data/lib/action_dispatch/http/permissions_policy.rb +173 -0
- data/lib/action_dispatch/http/request.rb +24 -8
- data/lib/action_dispatch/http/response.rb +17 -16
- data/lib/action_dispatch/http/url.rb +3 -2
- data/lib/action_dispatch/journey/formatter.rb +55 -30
- data/lib/action_dispatch/journey/gtg/builder.rb +22 -36
- data/lib/action_dispatch/journey/gtg/simulator.rb +8 -7
- data/lib/action_dispatch/journey/gtg/transition_table.rb +6 -4
- data/lib/action_dispatch/journey/nfa/dot.rb +0 -11
- data/lib/action_dispatch/journey/nodes/node.rb +4 -3
- data/lib/action_dispatch/journey/parser.rb +13 -13
- data/lib/action_dispatch/journey/parser.y +1 -1
- data/lib/action_dispatch/journey/path/pattern.rb +13 -18
- data/lib/action_dispatch/journey/route.rb +7 -18
- data/lib/action_dispatch/journey/router/utils.rb +6 -4
- data/lib/action_dispatch/journey/router.rb +26 -30
- data/lib/action_dispatch/journey/visitors.rb +1 -1
- data/lib/action_dispatch/journey.rb +0 -2
- data/lib/action_dispatch/middleware/actionable_exceptions.rb +1 -1
- data/lib/action_dispatch/middleware/cookies.rb +89 -46
- data/lib/action_dispatch/middleware/debug_exceptions.rb +8 -15
- data/lib/action_dispatch/middleware/debug_view.rb +1 -1
- data/lib/action_dispatch/middleware/exception_wrapper.rb +28 -16
- data/lib/action_dispatch/middleware/host_authorization.rb +63 -14
- data/lib/action_dispatch/middleware/remote_ip.rb +5 -4
- data/lib/action_dispatch/middleware/request_id.rb +4 -5
- data/lib/action_dispatch/middleware/session/abstract_store.rb +2 -2
- data/lib/action_dispatch/middleware/session/cookie_store.rb +2 -2
- data/lib/action_dispatch/middleware/show_exceptions.rb +12 -0
- data/lib/action_dispatch/middleware/ssl.rb +12 -7
- data/lib/action_dispatch/middleware/stack.rb +19 -1
- data/lib/action_dispatch/middleware/static.rb +154 -93
- data/lib/action_dispatch/middleware/templates/rescues/_message_and_suggestions.html.erb +22 -0
- data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +2 -5
- data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +2 -2
- data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +2 -2
- data/lib/action_dispatch/middleware/templates/rescues/layout.erb +100 -8
- data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +1 -1
- data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +21 -1
- data/lib/action_dispatch/railtie.rb +3 -2
- data/lib/action_dispatch/request/session.rb +2 -8
- data/lib/action_dispatch/request/utils.rb +26 -2
- data/lib/action_dispatch/routing/inspector.rb +8 -7
- data/lib/action_dispatch/routing/mapper.rb +102 -71
- data/lib/action_dispatch/routing/polymorphic_routes.rb +12 -11
- data/lib/action_dispatch/routing/redirection.rb +4 -4
- data/lib/action_dispatch/routing/route_set.rb +49 -41
- data/lib/action_dispatch/system_test_case.rb +35 -24
- data/lib/action_dispatch/system_testing/browser.rb +33 -27
- data/lib/action_dispatch/system_testing/driver.rb +6 -7
- data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +47 -6
- data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +4 -7
- data/lib/action_dispatch/testing/assertions/response.rb +2 -4
- data/lib/action_dispatch/testing/assertions/routing.rb +5 -5
- data/lib/action_dispatch/testing/assertions.rb +1 -1
- data/lib/action_dispatch/testing/integration.rb +40 -29
- data/lib/action_dispatch/testing/test_process.rb +32 -4
- data/lib/action_dispatch/testing/test_request.rb +3 -3
- data/lib/action_dispatch.rb +3 -2
- data/lib/action_pack/gem_version.rb +2 -2
- data/lib/action_pack.rb +1 -1
- metadata +18 -19
- data/lib/action_controller/metal/force_ssl.rb +0 -58
- data/lib/action_dispatch/http/parameter_filter.rb +0 -12
- data/lib/action_dispatch/journey/nfa/builder.rb +0 -78
- data/lib/action_dispatch/journey/nfa/simulator.rb +0 -47
- data/lib/action_dispatch/journey/nfa/transition_table.rb +0 -119
@@ -40,11 +40,12 @@ module ActionDispatch
|
|
40
40
|
req.path_info = "/" + req.path_info unless req.path_info.start_with? "/"
|
41
41
|
end
|
42
42
|
|
43
|
-
|
44
|
-
|
43
|
+
tmp_params = set_params.merge route.defaults
|
44
|
+
parameters.each_pair { |key, val|
|
45
|
+
tmp_params[key] = val.force_encoding(::Encoding::UTF_8)
|
45
46
|
}
|
46
47
|
|
47
|
-
req.path_parameters =
|
48
|
+
req.path_parameters = tmp_params
|
48
49
|
|
49
50
|
status, headers, body = route.app.serve(req)
|
50
51
|
|
@@ -65,7 +66,8 @@ module ActionDispatch
|
|
65
66
|
find_routes(rails_req).each do |match, parameters, route|
|
66
67
|
unless route.path.anchored
|
67
68
|
rails_req.script_name = match.to_s
|
68
|
-
rails_req.path_info = match.post_match
|
69
|
+
rails_req.path_info = match.post_match
|
70
|
+
rails_req.path_info = "/" + rails_req.path_info unless rails_req.path_info.start_with? "/"
|
69
71
|
end
|
70
72
|
|
71
73
|
parameters = route.defaults.merge parameters
|
@@ -105,23 +107,24 @@ module ActionDispatch
|
|
105
107
|
end
|
106
108
|
|
107
109
|
def find_routes(req)
|
108
|
-
|
109
|
-
|
110
|
+
path_info = req.path_info
|
111
|
+
routes = filter_routes(path_info).concat custom_routes.find_all { |r|
|
112
|
+
r.path.match?(path_info)
|
110
113
|
}
|
111
114
|
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
end
|
115
|
+
if req.head?
|
116
|
+
routes = match_head_routes(routes, req)
|
117
|
+
else
|
118
|
+
routes.select! { |r| r.matches?(req) }
|
119
|
+
end
|
118
120
|
|
119
121
|
routes.sort_by!(&:precedence)
|
120
122
|
|
121
123
|
routes.map! { |r|
|
122
|
-
match_data = r.path.match(
|
124
|
+
match_data = r.path.match(path_info)
|
123
125
|
path_parameters = {}
|
124
|
-
match_data.names.
|
126
|
+
match_data.names.each_with_index { |name, i|
|
127
|
+
val = match_data[i + 1]
|
125
128
|
path_parameters[name.to_sym] = Utils.unescape_uri(val) if val
|
126
129
|
}
|
127
130
|
[match_data, path_parameters, r]
|
@@ -129,24 +132,17 @@ module ActionDispatch
|
|
129
132
|
end
|
130
133
|
|
131
134
|
def match_head_routes(routes, req)
|
132
|
-
|
133
|
-
head_routes
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
end
|
142
|
-
else
|
143
|
-
head_routes
|
135
|
+
head_routes = routes.select { |r| r.requires_matching_verb? && r.matches?(req) }
|
136
|
+
return head_routes unless head_routes.empty?
|
137
|
+
|
138
|
+
begin
|
139
|
+
req.request_method = "GET"
|
140
|
+
routes.select! { |r| r.matches?(req) }
|
141
|
+
routes
|
142
|
+
ensure
|
143
|
+
req.request_method = "HEAD"
|
144
144
|
end
|
145
145
|
end
|
146
|
-
|
147
|
-
def match_routes(routes, req)
|
148
|
-
routes.select { |r| r.matches?(req) }
|
149
|
-
end
|
150
146
|
end
|
151
147
|
end
|
152
148
|
end
|
@@ -24,7 +24,7 @@ module ActionDispatch
|
|
24
24
|
|
25
25
|
private
|
26
26
|
def actionable_request?(request)
|
27
|
-
request.get_header("action_dispatch.show_detailed_exceptions")
|
27
|
+
request.get_header("action_dispatch.show_detailed_exceptions") && request.post? && request.path == endpoint
|
28
28
|
end
|
29
29
|
|
30
30
|
def redirect_to(location)
|
@@ -69,6 +69,10 @@ module ActionDispatch
|
|
69
69
|
get_header Cookies::COOKIES_SERIALIZER
|
70
70
|
end
|
71
71
|
|
72
|
+
def cookies_same_site_protection
|
73
|
+
get_header(Cookies::COOKIES_SAME_SITE_PROTECTION) || Proc.new { }
|
74
|
+
end
|
75
|
+
|
72
76
|
def cookies_digest
|
73
77
|
get_header Cookies::COOKIES_DIGEST
|
74
78
|
end
|
@@ -84,11 +88,10 @@ module ActionDispatch
|
|
84
88
|
# :startdoc:
|
85
89
|
end
|
86
90
|
|
87
|
-
#
|
91
|
+
# Read and write data to cookies through ActionController#cookies.
|
88
92
|
#
|
89
|
-
#
|
90
|
-
#
|
91
|
-
# the cookie object itself back, just the value it holds.
|
93
|
+
# When reading cookie data, the data is read from the HTTP request header, Cookie.
|
94
|
+
# When writing cookie data, the data is sent out in the HTTP response header, Set-Cookie.
|
92
95
|
#
|
93
96
|
# Examples of writing:
|
94
97
|
#
|
@@ -150,8 +153,10 @@ module ActionDispatch
|
|
150
153
|
# * <tt>:domain</tt> - The domain for which this cookie applies so you can
|
151
154
|
# restrict to the domain level. If you use a schema like www.example.com
|
152
155
|
# and want to share session with user.example.com set <tt>:domain</tt>
|
153
|
-
# to <tt>:all</tt>.
|
154
|
-
#
|
156
|
+
# to <tt>:all</tt>. To support multiple domains, provide an array, and
|
157
|
+
# the first domain matching <tt>request.host</tt> will be used. Make
|
158
|
+
# sure to specify the <tt>:domain</tt> option with <tt>:all</tt> or
|
159
|
+
# <tt>Array</tt> again when deleting cookies.
|
155
160
|
#
|
156
161
|
# domain: nil # Does not set cookie domain. (default)
|
157
162
|
# domain: :all # Allow the cookie for the top most level
|
@@ -181,6 +186,7 @@ module ActionDispatch
|
|
181
186
|
COOKIES_SERIALIZER = "action_dispatch.cookies_serializer"
|
182
187
|
COOKIES_DIGEST = "action_dispatch.cookies_digest"
|
183
188
|
COOKIES_ROTATIONS = "action_dispatch.cookies_rotations"
|
189
|
+
COOKIES_SAME_SITE_PROTECTION = "action_dispatch.cookies_same_site_protection"
|
184
190
|
USE_COOKIES_WITH_METADATA = "action_dispatch.use_cookies_with_metadata"
|
185
191
|
|
186
192
|
# Cookies can typically store 4096 bytes.
|
@@ -259,6 +265,12 @@ module ActionDispatch
|
|
259
265
|
request.use_authenticated_cookie_encryption
|
260
266
|
end
|
261
267
|
|
268
|
+
def prepare_upgrade_legacy_hmac_aes_cbc_cookies?
|
269
|
+
request.secret_key_base.present? &&
|
270
|
+
request.authenticated_encrypted_cookie_salt.present? &&
|
271
|
+
!request.use_authenticated_cookie_encryption
|
272
|
+
end
|
273
|
+
|
262
274
|
def encrypted_cookie_cipher
|
263
275
|
request.encrypted_cookie_cipher || "aes-256-gcm"
|
264
276
|
end
|
@@ -271,24 +283,10 @@ module ActionDispatch
|
|
271
283
|
class CookieJar #:nodoc:
|
272
284
|
include Enumerable, ChainedCookieJars
|
273
285
|
|
274
|
-
# This regular expression is used to split the levels of a domain.
|
275
|
-
# The top level domain can be any string without a period or
|
276
|
-
# **.**, ***.** style TLDs like co.uk or com.au
|
277
|
-
#
|
278
|
-
# www.example.co.uk gives:
|
279
|
-
# $& => example.co.uk
|
280
|
-
#
|
281
|
-
# example.com gives:
|
282
|
-
# $& => example.com
|
283
|
-
#
|
284
|
-
# lots.of.subdomains.example.local gives:
|
285
|
-
# $& => example.local
|
286
|
-
DOMAIN_REGEXP = /[^.]*\.([^.]*|..\...|...\...)$/
|
287
|
-
|
288
286
|
def self.build(req, cookies)
|
289
|
-
new(req)
|
290
|
-
|
291
|
-
|
287
|
+
jar = new(req)
|
288
|
+
jar.update(cookies)
|
289
|
+
jar
|
292
290
|
end
|
293
291
|
|
294
292
|
attr_reader :request
|
@@ -346,28 +344,6 @@ module ActionDispatch
|
|
346
344
|
@cookies.map { |k, v| "#{escape(k)}=#{escape(v)}" }.join "; "
|
347
345
|
end
|
348
346
|
|
349
|
-
def handle_options(options) # :nodoc:
|
350
|
-
if options[:expires].respond_to?(:from_now)
|
351
|
-
options[:expires] = options[:expires].from_now
|
352
|
-
end
|
353
|
-
|
354
|
-
options[:path] ||= "/"
|
355
|
-
|
356
|
-
if options[:domain] == :all || options[:domain] == "all"
|
357
|
-
# If there is a provided tld length then we use it otherwise default domain regexp.
|
358
|
-
domain_regexp = options[:tld_length] ? /([^.]+\.?){#{options[:tld_length]}}$/ : DOMAIN_REGEXP
|
359
|
-
|
360
|
-
# If host is not ip and matches domain regexp.
|
361
|
-
# (ip confirms to domain regexp so we explicitly check for ip)
|
362
|
-
options[:domain] = if (request.host !~ /^[\d.]+$/) && (request.host =~ domain_regexp)
|
363
|
-
".#{$&}"
|
364
|
-
end
|
365
|
-
elsif options[:domain].is_a? Array
|
366
|
-
# If host matches one of the supplied domains without a dot in front of it.
|
367
|
-
options[:domain] = options[:domain].find { |domain| request.host.include? domain.sub(/^\./, "") }
|
368
|
-
end
|
369
|
-
end
|
370
|
-
|
371
347
|
# Sets the cookie named +name+. The second argument may be the cookie's
|
372
348
|
# value or a hash of options as documented above.
|
373
349
|
def []=(name, options)
|
@@ -447,6 +423,56 @@ module ActionDispatch
|
|
447
423
|
def write_cookie?(cookie)
|
448
424
|
request.ssl? || !cookie[:secure] || always_write_cookie
|
449
425
|
end
|
426
|
+
|
427
|
+
def handle_options(options)
|
428
|
+
if options[:expires].respond_to?(:from_now)
|
429
|
+
options[:expires] = options[:expires].from_now
|
430
|
+
end
|
431
|
+
|
432
|
+
options[:path] ||= "/"
|
433
|
+
|
434
|
+
cookies_same_site_protection = request.cookies_same_site_protection
|
435
|
+
options[:same_site] ||= cookies_same_site_protection.call(request)
|
436
|
+
|
437
|
+
if options[:domain] == :all || options[:domain] == "all"
|
438
|
+
cookie_domain = ""
|
439
|
+
dot_splitted_host = request.host.split('.', -1)
|
440
|
+
|
441
|
+
# Case where request.host is not an IP address or it's an invalid domain
|
442
|
+
# (ip confirms to the domain structure we expect so we explicitly check for ip)
|
443
|
+
if request.host.match?(/^[\d.]+$/) || dot_splitted_host.include?("") || dot_splitted_host.length == 1
|
444
|
+
options[:domain] = nil
|
445
|
+
return
|
446
|
+
end
|
447
|
+
|
448
|
+
# If there is a provided tld length then we use it otherwise default domain.
|
449
|
+
if options[:tld_length].present?
|
450
|
+
# Case where the tld_length provided is valid
|
451
|
+
if dot_splitted_host.length >= options[:tld_length]
|
452
|
+
cookie_domain = dot_splitted_host.last(options[:tld_length]).join('.')
|
453
|
+
end
|
454
|
+
# Case where tld_length is not provided
|
455
|
+
else
|
456
|
+
# Regular TLDs
|
457
|
+
if !(/([^.]{2,3}\.[^.]{2})$/.match?(request.host))
|
458
|
+
cookie_domain = dot_splitted_host.last(2).join('.')
|
459
|
+
# **.**, ***.** style TLDs like co.uk and com.au
|
460
|
+
else
|
461
|
+
cookie_domain = dot_splitted_host.last(3).join('.')
|
462
|
+
end
|
463
|
+
end
|
464
|
+
|
465
|
+
options[:domain] = if cookie_domain.present?
|
466
|
+
".#{cookie_domain}"
|
467
|
+
end
|
468
|
+
elsif options[:domain].is_a? Array
|
469
|
+
# If host matches one of the supplied domains.
|
470
|
+
options[:domain] = options[:domain].find do |domain|
|
471
|
+
domain = domain.delete_prefix(".")
|
472
|
+
request.host == domain || request.host.end_with?(".#{domain}")
|
473
|
+
end
|
474
|
+
end
|
475
|
+
end
|
450
476
|
end
|
451
477
|
|
452
478
|
class AbstractCookieJar # :nodoc:
|
@@ -508,6 +534,18 @@ module ActionDispatch
|
|
508
534
|
end
|
509
535
|
end
|
510
536
|
|
537
|
+
class MarshalWithJsonFallback # :nodoc:
|
538
|
+
def self.load(value)
|
539
|
+
Marshal.load(value)
|
540
|
+
rescue TypeError => e
|
541
|
+
ActiveSupport::JSON.decode(value) rescue raise e
|
542
|
+
end
|
543
|
+
|
544
|
+
def self.dump(value)
|
545
|
+
Marshal.dump(value)
|
546
|
+
end
|
547
|
+
end
|
548
|
+
|
511
549
|
class JsonSerializer # :nodoc:
|
512
550
|
def self.load(value)
|
513
551
|
ActiveSupport::JSON.decode(value)
|
@@ -555,7 +593,7 @@ module ActionDispatch
|
|
555
593
|
serializer = request.cookies_serializer || :marshal
|
556
594
|
case serializer
|
557
595
|
when :marshal
|
558
|
-
|
596
|
+
MarshalWithJsonFallback
|
559
597
|
when :json, :hybrid
|
560
598
|
JsonSerializer
|
561
599
|
else
|
@@ -625,6 +663,11 @@ module ActionDispatch
|
|
625
663
|
sign_secret = request.key_generator.generate_key(request.encrypted_signed_cookie_salt)
|
626
664
|
|
627
665
|
@encryptor.rotate(secret, sign_secret, cipher: legacy_cipher, digest: digest, serializer: SERIALIZER)
|
666
|
+
elsif prepare_upgrade_legacy_hmac_aes_cbc_cookies?
|
667
|
+
future_cipher = encrypted_cookie_cipher
|
668
|
+
secret = request.key_generator.generate_key(request.authenticated_encrypted_cookie_salt, ActiveSupport::MessageEncryptor.key_len(future_cipher))
|
669
|
+
|
670
|
+
@encryptor.rotate(secret, nil, cipher: future_cipher, serializer: SERIALIZER)
|
628
671
|
end
|
629
672
|
end
|
630
673
|
|
@@ -4,10 +4,7 @@ require "action_dispatch/http/request"
|
|
4
4
|
require "action_dispatch/middleware/exception_wrapper"
|
5
5
|
require "action_dispatch/routing/inspector"
|
6
6
|
|
7
|
-
require "active_support/actionable_error"
|
8
|
-
|
9
7
|
require "action_view"
|
10
|
-
require "action_view/base"
|
11
8
|
|
12
9
|
module ActionDispatch
|
13
10
|
# This middleware is responsible for logging exceptions and
|
@@ -140,20 +137,16 @@ module ActionDispatch
|
|
140
137
|
return unless logger
|
141
138
|
|
142
139
|
exception = wrapper.exception
|
140
|
+
trace = wrapper.exception_trace
|
143
141
|
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
message << "#{exception.class} (#{exception.message}):"
|
151
|
-
message.concat(exception.annotated_source_code) if exception.respond_to?(:annotated_source_code)
|
152
|
-
message << " "
|
153
|
-
message.concat(trace)
|
142
|
+
message = []
|
143
|
+
message << " "
|
144
|
+
message << "#{exception.class} (#{exception.message}):"
|
145
|
+
message.concat(exception.annotated_source_code) if exception.respond_to?(:annotated_source_code)
|
146
|
+
message << " "
|
147
|
+
message.concat(trace)
|
154
148
|
|
155
|
-
|
156
|
-
end
|
149
|
+
log_array(logger, message)
|
157
150
|
end
|
158
151
|
|
159
152
|
def log_array(logger, array)
|
@@ -6,21 +6,21 @@ require "rack/utils"
|
|
6
6
|
module ActionDispatch
|
7
7
|
class ExceptionWrapper
|
8
8
|
cattr_accessor :rescue_responses, default: Hash.new(:internal_server_error).merge!(
|
9
|
-
"ActionController::RoutingError"
|
10
|
-
"AbstractController::ActionNotFound"
|
11
|
-
"ActionController::MethodNotAllowed"
|
12
|
-
"ActionController::UnknownHttpMethod"
|
13
|
-
"ActionController::NotImplemented"
|
14
|
-
"ActionController::UnknownFormat"
|
9
|
+
"ActionController::RoutingError" => :not_found,
|
10
|
+
"AbstractController::ActionNotFound" => :not_found,
|
11
|
+
"ActionController::MethodNotAllowed" => :method_not_allowed,
|
12
|
+
"ActionController::UnknownHttpMethod" => :method_not_allowed,
|
13
|
+
"ActionController::NotImplemented" => :not_implemented,
|
14
|
+
"ActionController::UnknownFormat" => :not_acceptable,
|
15
15
|
"ActionDispatch::Http::MimeNegotiation::InvalidType" => :not_acceptable,
|
16
|
-
"ActionController::MissingExactTemplate"
|
17
|
-
"ActionController::InvalidAuthenticityToken"
|
18
|
-
"ActionController::InvalidCrossOriginRequest"
|
19
|
-
"ActionDispatch::Http::Parameters::ParseError"
|
20
|
-
"ActionController::BadRequest"
|
21
|
-
"ActionController::ParameterMissing"
|
22
|
-
"Rack::QueryParser::ParameterTypeError"
|
23
|
-
"Rack::QueryParser::InvalidParameterError"
|
16
|
+
"ActionController::MissingExactTemplate" => :not_acceptable,
|
17
|
+
"ActionController::InvalidAuthenticityToken" => :unprocessable_entity,
|
18
|
+
"ActionController::InvalidCrossOriginRequest" => :unprocessable_entity,
|
19
|
+
"ActionDispatch::Http::Parameters::ParseError" => :bad_request,
|
20
|
+
"ActionController::BadRequest" => :bad_request,
|
21
|
+
"ActionController::ParameterMissing" => :bad_request,
|
22
|
+
"Rack::QueryParser::ParameterTypeError" => :bad_request,
|
23
|
+
"Rack::QueryParser::InvalidParameterError" => :bad_request
|
24
24
|
)
|
25
25
|
|
26
26
|
cattr_accessor :rescue_templates, default: Hash.new("diagnostics").merge!(
|
@@ -36,18 +36,24 @@ module ActionDispatch
|
|
36
36
|
"ActionView::Template::Error"
|
37
37
|
]
|
38
38
|
|
39
|
+
cattr_accessor :silent_exceptions, default: [
|
40
|
+
"ActionController::RoutingError",
|
41
|
+
"ActionDispatch::Http::MimeNegotiation::InvalidType"
|
42
|
+
]
|
43
|
+
|
39
44
|
attr_reader :backtrace_cleaner, :exception, :wrapped_causes, :line_number, :file
|
40
45
|
|
41
46
|
def initialize(backtrace_cleaner, exception)
|
42
47
|
@backtrace_cleaner = backtrace_cleaner
|
43
48
|
@exception = exception
|
49
|
+
@exception_class_name = @exception.class.name
|
44
50
|
@wrapped_causes = wrapped_causes_for(exception, backtrace_cleaner)
|
45
51
|
|
46
52
|
expand_backtrace if exception.is_a?(SyntaxError) || exception.cause.is_a?(SyntaxError)
|
47
53
|
end
|
48
54
|
|
49
55
|
def unwrapped_exception
|
50
|
-
if wrapper_exceptions.include?(
|
56
|
+
if wrapper_exceptions.include?(@exception_class_name)
|
51
57
|
exception.cause
|
52
58
|
else
|
53
59
|
exception
|
@@ -55,13 +61,19 @@ module ActionDispatch
|
|
55
61
|
end
|
56
62
|
|
57
63
|
def rescue_template
|
58
|
-
@@rescue_templates[@
|
64
|
+
@@rescue_templates[@exception_class_name]
|
59
65
|
end
|
60
66
|
|
61
67
|
def status_code
|
62
68
|
self.class.status_code_for_exception(unwrapped_exception.class.name)
|
63
69
|
end
|
64
70
|
|
71
|
+
def exception_trace
|
72
|
+
trace = application_trace
|
73
|
+
trace = framework_trace if trace.empty? && !silent_exceptions.include?(@exception_class_name)
|
74
|
+
trace
|
75
|
+
end
|
76
|
+
|
65
77
|
def application_trace
|
66
78
|
clean_backtrace(:silent)
|
67
79
|
end
|
@@ -4,11 +4,19 @@ require "action_dispatch/http/request"
|
|
4
4
|
|
5
5
|
module ActionDispatch
|
6
6
|
# This middleware guards from DNS rebinding attacks by explicitly permitting
|
7
|
-
# the hosts a request can be sent to
|
7
|
+
# the hosts a request can be sent to, and is passed the options set in
|
8
|
+
# +config.host_authorization+.
|
9
|
+
#
|
10
|
+
# Requests can opt-out of Host Authorization with +exclude+:
|
11
|
+
#
|
12
|
+
# config.host_authorization = { exclude: ->(request) { request.path =~ /healthcheck/ } }
|
8
13
|
#
|
9
14
|
# When a request comes to an unauthorized host, the +response_app+
|
10
15
|
# application will be executed and rendered. If no +response_app+ is given, a
|
11
|
-
# default one will run
|
16
|
+
# default one will run.
|
17
|
+
# The default response app logs blocked host info with level 'error' and
|
18
|
+
# responds with <tt>403 Forbidden</tt>. The body of the response contains debug info
|
19
|
+
# if +config.consider_all_requests_local+ is set to true, otherwise the body is empty.
|
12
20
|
class HostAuthorization
|
13
21
|
ALLOWED_HOSTS_IN_DEVELOPMENT = [".localhost", IPAddr.new("0.0.0.0/0"), IPAddr.new("::/0")]
|
14
22
|
PORT_REGEX = /(?::\d+)/ # :nodoc:
|
@@ -74,23 +82,60 @@ module ActionDispatch
|
|
74
82
|
end
|
75
83
|
end
|
76
84
|
|
77
|
-
|
78
|
-
|
85
|
+
class DefaultResponseApp # :nodoc:
|
86
|
+
RESPONSE_STATUS = 403
|
87
|
+
|
88
|
+
def call(env)
|
89
|
+
request = Request.new(env)
|
90
|
+
format = request.xhr? ? "text/plain" : "text/html"
|
91
|
+
|
92
|
+
log_error(request)
|
93
|
+
response(format, response_body(request))
|
94
|
+
end
|
95
|
+
|
96
|
+
private
|
97
|
+
def response_body(request)
|
98
|
+
return "" unless request.get_header("action_dispatch.show_detailed_exceptions")
|
99
|
+
|
100
|
+
template = DebugView.new(host: request.host)
|
101
|
+
template.render(template: "rescues/blocked_host", layout: "rescues/layout")
|
102
|
+
end
|
103
|
+
|
104
|
+
def response(format, body)
|
105
|
+
[RESPONSE_STATUS,
|
106
|
+
{ "Content-Type" => "#{format}; charset=#{Response.default_charset}",
|
107
|
+
"Content-Length" => body.bytesize.to_s },
|
108
|
+
[body]]
|
109
|
+
end
|
110
|
+
|
111
|
+
def log_error(request)
|
112
|
+
logger = available_logger(request)
|
113
|
+
|
114
|
+
return unless logger
|
79
115
|
|
80
|
-
|
81
|
-
|
82
|
-
body = template.render(template: "rescues/blocked_host", layout: "rescues/layout")
|
116
|
+
logger.error("[#{self.class.name}] Blocked host: #{request.host}")
|
117
|
+
end
|
83
118
|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
}, [body]]
|
119
|
+
def available_logger(request)
|
120
|
+
request.logger || ActionView::Base.logger
|
121
|
+
end
|
88
122
|
end
|
89
123
|
|
90
|
-
def initialize(app, hosts,
|
124
|
+
def initialize(app, hosts, deprecated_response_app = nil, exclude: nil, response_app: nil)
|
91
125
|
@app = app
|
92
126
|
@permissions = Permissions.new(hosts)
|
93
|
-
@
|
127
|
+
@exclude = exclude
|
128
|
+
|
129
|
+
unless deprecated_response_app.nil?
|
130
|
+
ActiveSupport::Deprecation.warn(<<-MSG.squish)
|
131
|
+
`action_dispatch.hosts_response_app` is deprecated and will be ignored in Rails 7.0.
|
132
|
+
Use the Host Authorization `response_app` setting instead.
|
133
|
+
MSG
|
134
|
+
|
135
|
+
response_app ||= deprecated_response_app
|
136
|
+
end
|
137
|
+
|
138
|
+
@response_app = response_app || DefaultResponseApp.new
|
94
139
|
end
|
95
140
|
|
96
141
|
def call(env)
|
@@ -98,7 +143,7 @@ module ActionDispatch
|
|
98
143
|
|
99
144
|
request = Request.new(env)
|
100
145
|
|
101
|
-
if authorized?(request)
|
146
|
+
if authorized?(request) || excluded?(request)
|
102
147
|
mark_as_authorized(request)
|
103
148
|
@app.call(env)
|
104
149
|
else
|
@@ -114,6 +159,10 @@ module ActionDispatch
|
|
114
159
|
@permissions.allows?(origin_host) && (forwarded_host.blank? || @permissions.allows?(forwarded_host))
|
115
160
|
end
|
116
161
|
|
162
|
+
def excluded?(request)
|
163
|
+
@exclude && @exclude.call(request)
|
164
|
+
end
|
165
|
+
|
117
166
|
def mark_as_authorized(request)
|
118
167
|
request.set_header("action_dispatch.authorized_host", request.host)
|
119
168
|
end
|
@@ -33,7 +33,7 @@ module ActionDispatch
|
|
33
33
|
# not be the ultimate client IP in production, and so are discarded. See
|
34
34
|
# https://en.wikipedia.org/wiki/Private_network for details.
|
35
35
|
TRUSTED_PROXIES = [
|
36
|
-
"127.0.0.
|
36
|
+
"127.0.0.0/8", # localhost IPv4 range, per RFC-3330
|
37
37
|
"::1", # localhost IPv6
|
38
38
|
"fc00::/7", # private IPv6 range fc00::/7
|
39
39
|
"10.0.0.0/8", # private IPv4 range 10.x.x.x
|
@@ -143,10 +143,11 @@ module ActionDispatch
|
|
143
143
|
# - X-Forwarded-For will be a list of IPs, one per proxy, or blank
|
144
144
|
# - Client-Ip is propagated from the outermost proxy, or is blank
|
145
145
|
# - REMOTE_ADDR will be the IP that made the request to Rack
|
146
|
-
ips = [forwarded_ips, client_ips
|
146
|
+
ips = [forwarded_ips, client_ips].flatten.compact
|
147
147
|
|
148
|
-
# If every single IP option is in the trusted list,
|
149
|
-
|
148
|
+
# If every single IP option is in the trusted list, return the IP
|
149
|
+
# that's furthest away
|
150
|
+
filter_proxies(ips + [remote_addr]).first || ips.last || remote_addr
|
150
151
|
end
|
151
152
|
|
152
153
|
# Memoizes the value returned by #calculate_ip and returns it for
|
@@ -15,16 +15,15 @@ module ActionDispatch
|
|
15
15
|
# The unique request id can be used to trace a request end-to-end and would typically end up being part of log files
|
16
16
|
# from multiple pieces of the stack.
|
17
17
|
class RequestId
|
18
|
-
|
19
|
-
|
20
|
-
def initialize(app)
|
18
|
+
def initialize(app, header:)
|
21
19
|
@app = app
|
20
|
+
@header = header
|
22
21
|
end
|
23
22
|
|
24
23
|
def call(env)
|
25
24
|
req = ActionDispatch::Request.new env
|
26
|
-
req.request_id = make_request_id(req.
|
27
|
-
@app.call(env).tap { |_status, headers, _body| headers[
|
25
|
+
req.request_id = make_request_id(req.headers[@header])
|
26
|
+
@app.call(env).tap { |_status, headers, _body| headers[@header] = req.request_id }
|
28
27
|
end
|
29
28
|
|
30
29
|
private
|
@@ -82,7 +82,7 @@ module ActionDispatch
|
|
82
82
|
include SessionObject
|
83
83
|
|
84
84
|
private
|
85
|
-
def set_cookie(request,
|
85
|
+
def set_cookie(request, response, cookie)
|
86
86
|
request.cookie_jar[key] = cookie
|
87
87
|
end
|
88
88
|
end
|
@@ -97,7 +97,7 @@ module ActionDispatch
|
|
97
97
|
end
|
98
98
|
|
99
99
|
private
|
100
|
-
def set_cookie(request,
|
100
|
+
def set_cookie(request, response, cookie)
|
101
101
|
request.cookie_jar[key] = cookie
|
102
102
|
end
|
103
103
|
end
|
@@ -10,8 +10,8 @@ module ActionDispatch
|
|
10
10
|
# dramatically faster than the alternatives.
|
11
11
|
#
|
12
12
|
# Sessions typically contain at most a user_id and flash message; both fit
|
13
|
-
# within the
|
14
|
-
# you attempt to store more than
|
13
|
+
# within the 4096 bytes cookie size limit. A CookieOverflow exception is raised if
|
14
|
+
# you attempt to store more than 4096 bytes of data.
|
15
15
|
#
|
16
16
|
# The cookie jar used for storage is automatically configured to be the
|
17
17
|
# best possible option given your application's configuration.
|