actionpack 6.0.3.7 → 6.1.3.2

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.

Files changed (118) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +287 -226
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +1 -1
  5. data/lib/abstract_controller.rb +1 -0
  6. data/lib/abstract_controller/base.rb +35 -2
  7. data/lib/abstract_controller/callbacks.rb +2 -2
  8. data/lib/abstract_controller/helpers.rb +105 -90
  9. data/lib/abstract_controller/railties/routes_helpers.rb +17 -1
  10. data/lib/abstract_controller/rendering.rb +9 -9
  11. data/lib/abstract_controller/translation.rb +8 -2
  12. data/lib/action_controller.rb +2 -3
  13. data/lib/action_controller/api.rb +2 -2
  14. data/lib/action_controller/base.rb +4 -2
  15. data/lib/action_controller/caching.rb +0 -1
  16. data/lib/action_controller/log_subscriber.rb +3 -3
  17. data/lib/action_controller/metal.rb +2 -2
  18. data/lib/action_controller/metal/conditional_get.rb +11 -3
  19. data/lib/action_controller/metal/content_security_policy.rb +1 -1
  20. data/lib/action_controller/metal/cookies.rb +3 -1
  21. data/lib/action_controller/metal/data_streaming.rb +1 -1
  22. data/lib/action_controller/metal/etag_with_template_digest.rb +2 -4
  23. data/lib/action_controller/metal/exceptions.rb +33 -0
  24. data/lib/action_controller/metal/head.rb +7 -4
  25. data/lib/action_controller/metal/helpers.rb +11 -1
  26. data/lib/action_controller/metal/http_authentication.rb +4 -2
  27. data/lib/action_controller/metal/implicit_render.rb +1 -1
  28. data/lib/action_controller/metal/instrumentation.rb +11 -9
  29. data/lib/action_controller/metal/live.rb +1 -1
  30. data/lib/action_controller/metal/logging.rb +20 -0
  31. data/lib/action_controller/metal/mime_responds.rb +6 -2
  32. data/lib/action_controller/metal/parameter_encoding.rb +35 -4
  33. data/lib/action_controller/metal/params_wrapper.rb +14 -8
  34. data/lib/action_controller/metal/permissions_policy.rb +46 -0
  35. data/lib/action_controller/metal/redirecting.rb +1 -1
  36. data/lib/action_controller/metal/rendering.rb +6 -0
  37. data/lib/action_controller/metal/request_forgery_protection.rb +48 -24
  38. data/lib/action_controller/metal/rescue.rb +1 -1
  39. data/lib/action_controller/metal/strong_parameters.rb +103 -15
  40. data/lib/action_controller/renderer.rb +24 -13
  41. data/lib/action_controller/test_case.rb +62 -56
  42. data/lib/action_dispatch.rb +3 -2
  43. data/lib/action_dispatch/http/cache.rb +12 -10
  44. data/lib/action_dispatch/http/content_disposition.rb +2 -2
  45. data/lib/action_dispatch/http/content_security_policy.rb +5 -1
  46. data/lib/action_dispatch/http/filter_parameters.rb +1 -1
  47. data/lib/action_dispatch/http/filter_redirect.rb +1 -1
  48. data/lib/action_dispatch/http/headers.rb +3 -2
  49. data/lib/action_dispatch/http/mime_negotiation.rb +20 -8
  50. data/lib/action_dispatch/http/mime_type.rb +29 -16
  51. data/lib/action_dispatch/http/parameters.rb +1 -19
  52. data/lib/action_dispatch/http/permissions_policy.rb +173 -0
  53. data/lib/action_dispatch/http/request.rb +26 -8
  54. data/lib/action_dispatch/http/response.rb +17 -16
  55. data/lib/action_dispatch/http/url.rb +3 -2
  56. data/lib/action_dispatch/journey.rb +0 -2
  57. data/lib/action_dispatch/journey/formatter.rb +53 -28
  58. data/lib/action_dispatch/journey/gtg/builder.rb +22 -36
  59. data/lib/action_dispatch/journey/gtg/simulator.rb +8 -7
  60. data/lib/action_dispatch/journey/gtg/transition_table.rb +6 -4
  61. data/lib/action_dispatch/journey/nfa/dot.rb +0 -11
  62. data/lib/action_dispatch/journey/nodes/node.rb +4 -3
  63. data/lib/action_dispatch/journey/parser.rb +13 -13
  64. data/lib/action_dispatch/journey/parser.y +1 -1
  65. data/lib/action_dispatch/journey/path/pattern.rb +13 -18
  66. data/lib/action_dispatch/journey/route.rb +7 -18
  67. data/lib/action_dispatch/journey/router.rb +26 -30
  68. data/lib/action_dispatch/journey/router/utils.rb +6 -4
  69. data/lib/action_dispatch/middleware/actionable_exceptions.rb +2 -2
  70. data/lib/action_dispatch/middleware/cookies.rb +74 -33
  71. data/lib/action_dispatch/middleware/debug_exceptions.rb +10 -17
  72. data/lib/action_dispatch/middleware/debug_view.rb +1 -1
  73. data/lib/action_dispatch/middleware/exception_wrapper.rb +29 -17
  74. data/lib/action_dispatch/middleware/host_authorization.rb +27 -7
  75. data/lib/action_dispatch/middleware/public_exceptions.rb +1 -1
  76. data/lib/action_dispatch/middleware/remote_ip.rb +5 -4
  77. data/lib/action_dispatch/middleware/request_id.rb +4 -5
  78. data/lib/action_dispatch/middleware/session/abstract_store.rb +2 -2
  79. data/lib/action_dispatch/middleware/session/cookie_store.rb +2 -2
  80. data/lib/action_dispatch/middleware/show_exceptions.rb +2 -0
  81. data/lib/action_dispatch/middleware/ssl.rb +12 -7
  82. data/lib/action_dispatch/middleware/stack.rb +19 -1
  83. data/lib/action_dispatch/middleware/static.rb +154 -93
  84. data/lib/action_dispatch/middleware/templates/rescues/_message_and_suggestions.html.erb +22 -0
  85. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +2 -5
  86. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +2 -2
  87. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +2 -2
  88. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +100 -8
  89. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +1 -1
  90. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +21 -1
  91. data/lib/action_dispatch/railtie.rb +3 -2
  92. data/lib/action_dispatch/request/session.rb +2 -8
  93. data/lib/action_dispatch/request/utils.rb +26 -2
  94. data/lib/action_dispatch/routing/inspector.rb +8 -7
  95. data/lib/action_dispatch/routing/mapper.rb +102 -71
  96. data/lib/action_dispatch/routing/polymorphic_routes.rb +12 -11
  97. data/lib/action_dispatch/routing/redirection.rb +4 -4
  98. data/lib/action_dispatch/routing/route_set.rb +49 -41
  99. data/lib/action_dispatch/routing/url_for.rb +1 -0
  100. data/lib/action_dispatch/system_test_case.rb +29 -24
  101. data/lib/action_dispatch/system_testing/browser.rb +33 -27
  102. data/lib/action_dispatch/system_testing/driver.rb +6 -7
  103. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +47 -6
  104. data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +4 -7
  105. data/lib/action_dispatch/testing/assertions.rb +1 -1
  106. data/lib/action_dispatch/testing/assertions/response.rb +2 -4
  107. data/lib/action_dispatch/testing/assertions/routing.rb +5 -5
  108. data/lib/action_dispatch/testing/integration.rb +40 -29
  109. data/lib/action_dispatch/testing/test_process.rb +29 -4
  110. data/lib/action_dispatch/testing/test_request.rb +3 -3
  111. data/lib/action_pack.rb +1 -1
  112. data/lib/action_pack/gem_version.rb +2 -2
  113. metadata +16 -17
  114. data/lib/action_controller/metal/force_ssl.rb +0 -58
  115. data/lib/action_dispatch/http/parameter_filter.rb +0 -12
  116. data/lib/action_dispatch/journey/nfa/builder.rb +0 -78
  117. data/lib/action_dispatch/journey/nfa/simulator.rb +0 -47
  118. data/lib/action_dispatch/journey/nfa/transition_table.rb +0 -119
@@ -19,11 +19,13 @@ module ActionDispatch
19
19
  encoding = path.encoding
20
20
  path = +"/#{path}"
21
21
  path.squeeze!("/")
22
- path.sub!(%r{/+\Z}, "")
23
- path.gsub!(/(%[a-f0-9]{2})/) { $1.upcase }
24
- path = +"/" if path == ""
22
+
23
+ unless path == "/"
24
+ path.delete_suffix!("/")
25
+ path.gsub!(/(%[a-f0-9]{2})/) { $1.upcase }
26
+ end
27
+
25
28
  path.force_encoding(encoding)
26
- path
27
29
  end
28
30
 
29
31
  # URI path and fragment escaping
@@ -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") && request.post? && request.path == endpoint
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)
@@ -33,7 +33,7 @@ module ActionDispatch
33
33
  if uri.relative? || uri.scheme == "http" || uri.scheme == "https"
34
34
  body = "<html><body>You are being <a href=\"#{ERB::Util.unwrapped_html_escape(location)}\">redirected</a>.</body></html>"
35
35
  else
36
- return [400, {"Content-Type" => "text/plain"}, ["Invalid redirection URI"]]
36
+ return [400, { "Content-Type" => "text/plain" }, ["Invalid redirection URI"]]
37
37
  end
38
38
 
39
39
  [302, {
@@ -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
- # \Cookies are read and written through ActionController#cookies.
91
+ # Read and write data to cookies through ActionController#cookies.
88
92
  #
89
- # The cookies being read are the ones received along with the request, the cookies
90
- # being written will be sent out with the response. Reading a cookie does not get
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>. Make sure to specify the <tt>:domain</tt> option with
154
- # <tt>:all</tt> or <tt>Array</tt> again when deleting cookies.
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
@@ -286,9 +298,9 @@ module ActionDispatch
286
298
  DOMAIN_REGEXP = /[^.]*\.([^.]*|..\...|...\...)$/
287
299
 
288
300
  def self.build(req, cookies)
289
- new(req).tap do |hash|
290
- hash.update(cookies)
291
- end
301
+ jar = new(req)
302
+ jar.update(cookies)
303
+ jar
292
304
  end
293
305
 
294
306
  attr_reader :request
@@ -346,28 +358,6 @@ module ActionDispatch
346
358
  @cookies.map { |k, v| "#{escape(k)}=#{escape(v)}" }.join "; "
347
359
  end
348
360
 
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
361
  # Sets the cookie named +name+. The second argument may be the cookie's
372
362
  # value or a hash of options as documented above.
373
363
  def []=(name, options)
@@ -447,6 +437,34 @@ module ActionDispatch
447
437
  def write_cookie?(cookie)
448
438
  request.ssl? || !cookie[:secure] || always_write_cookie
449
439
  end
440
+
441
+ def handle_options(options)
442
+ if options[:expires].respond_to?(:from_now)
443
+ options[:expires] = options[:expires].from_now
444
+ end
445
+
446
+ options[:path] ||= "/"
447
+
448
+ cookies_same_site_protection = request.cookies_same_site_protection
449
+ options[:same_site] ||= cookies_same_site_protection.call(request)
450
+
451
+ if options[:domain] == :all || options[:domain] == "all"
452
+ # If there is a provided tld length then we use it otherwise default domain regexp.
453
+ domain_regexp = options[:tld_length] ? /([^.]+\.?){#{options[:tld_length]}}$/ : DOMAIN_REGEXP
454
+
455
+ # If host is not ip and matches domain regexp.
456
+ # (ip confirms to domain regexp so we explicitly check for ip)
457
+ options[:domain] = if !request.host.match?(/^[\d.]+$/) && (request.host =~ domain_regexp)
458
+ ".#{$&}"
459
+ end
460
+ elsif options[:domain].is_a? Array
461
+ # If host matches one of the supplied domains.
462
+ options[:domain] = options[:domain].find do |domain|
463
+ domain = domain.delete_prefix(".")
464
+ request.host == domain || request.host.end_with?(".#{domain}")
465
+ end
466
+ end
467
+ end
450
468
  end
451
469
 
452
470
  class AbstractCookieJar # :nodoc:
@@ -458,7 +476,13 @@ module ActionDispatch
458
476
 
459
477
  def [](name)
460
478
  if data = @parent_jar[name.to_s]
461
- parse(name, data, purpose: "cookie.#{name}") || parse(name, data)
479
+ result = parse(name, data, purpose: "cookie.#{name}")
480
+
481
+ if result.nil?
482
+ parse(name, data)
483
+ else
484
+ result
485
+ end
462
486
  end
463
487
  end
464
488
 
@@ -502,6 +526,18 @@ module ActionDispatch
502
526
  end
503
527
  end
504
528
 
529
+ class MarshalWithJsonFallback # :nodoc:
530
+ def self.load(value)
531
+ Marshal.load(value)
532
+ rescue TypeError => e
533
+ ActiveSupport::JSON.decode(value) rescue raise e
534
+ end
535
+
536
+ def self.dump(value)
537
+ Marshal.dump(value)
538
+ end
539
+ end
540
+
505
541
  class JsonSerializer # :nodoc:
506
542
  def self.load(value)
507
543
  ActiveSupport::JSON.decode(value)
@@ -549,7 +585,7 @@ module ActionDispatch
549
585
  serializer = request.cookies_serializer || :marshal
550
586
  case serializer
551
587
  when :marshal
552
- Marshal
588
+ MarshalWithJsonFallback
553
589
  when :json, :hybrid
554
590
  JsonSerializer
555
591
  else
@@ -619,6 +655,11 @@ module ActionDispatch
619
655
  sign_secret = request.key_generator.generate_key(request.encrypted_signed_cookie_salt)
620
656
 
621
657
  @encryptor.rotate(secret, sign_secret, cipher: legacy_cipher, digest: digest, serializer: SERIALIZER)
658
+ elsif prepare_upgrade_legacy_hmac_aes_cbc_cookies?
659
+ future_cipher = encrypted_cookie_cipher
660
+ secret = request.key_generator.generate_key(request.authenticated_encrypted_cookie_salt, ActiveSupport::MessageEncryptor.key_len(future_cipher))
661
+
662
+ @encryptor.rotate(secret, nil, cipher: future_cipher, serializer: SERIALIZER)
622
663
  end
623
664
  end
624
665
 
@@ -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
@@ -63,8 +60,8 @@ module ActionDispatch
63
60
  if request.get_header("action_dispatch.show_detailed_exceptions")
64
61
  begin
65
62
  content_type = request.formats.first
66
- rescue Mime::Type::InvalidMimeType
67
- render_for_api_request(Mime[:text], wrapper)
63
+ rescue ActionDispatch::Http::MimeNegotiation::InvalidType
64
+ content_type = Mime[:text]
68
65
  end
69
66
 
70
67
  if api_request?(content_type)
@@ -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
- trace = wrapper.application_trace
145
- trace = wrapper.framework_trace if trace.empty?
146
-
147
- ActiveSupport::Deprecation.silence do
148
- message = []
149
- message << " "
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
- log_array(logger, message)
156
- end
149
+ log_array(logger, message)
157
150
  end
158
151
 
159
152
  def log_array(logger, array)
@@ -12,7 +12,7 @@ module ActionDispatch
12
12
  def initialize(assigns)
13
13
  paths = [RESCUES_TEMPLATE_PATH]
14
14
  lookup_context = ActionView::LookupContext.new(paths)
15
- super(lookup_context, assigns)
15
+ super(lookup_context, assigns, nil)
16
16
  end
17
17
 
18
18
  def compiled_method_container
@@ -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" => :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
- "Mime::Type::InvalidMimeType" => :not_acceptable,
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
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
+ "ActionDispatch::Http::MimeNegotiation::InvalidType" => :not_acceptable,
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?(exception.class.to_s)
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[@exception.class.name]
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,16 @@ 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, which responds with +403 Forbidden+.
16
+ # default one will run, which responds with <tt>403 Forbidden</tt>.
12
17
  class HostAuthorization
13
18
  class Permissions # :nodoc:
14
19
  def initialize(hosts)
@@ -46,9 +51,9 @@ module ActionDispatch
46
51
 
47
52
  def sanitize_string(host)
48
53
  if host.start_with?(".")
49
- /\A(.+\.)?#{Regexp.escape(host[1..-1])}\z/
54
+ /\A(.+\.)?#{Regexp.escape(host[1..-1])}\z/i
50
55
  else
51
- host
56
+ /\A#{Regexp.escape host}\z/i
52
57
  end
53
58
  end
54
59
  end
@@ -66,9 +71,20 @@ module ActionDispatch
66
71
  }, [body]]
67
72
  end
68
73
 
69
- def initialize(app, hosts, response_app = nil)
74
+ def initialize(app, hosts, deprecated_response_app = nil, exclude: nil, response_app: nil)
70
75
  @app = app
71
76
  @permissions = Permissions.new(hosts)
77
+ @exclude = exclude
78
+
79
+ unless deprecated_response_app.nil?
80
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
81
+ `action_dispatch.hosts_response_app` is deprecated and will be ignored in Rails 6.2.
82
+ Use the Host Authorization `response_app` setting instead.
83
+ MSG
84
+
85
+ response_app ||= deprecated_response_app
86
+ end
87
+
72
88
  @response_app = response_app || DEFAULT_RESPONSE_APP
73
89
  end
74
90
 
@@ -77,7 +93,7 @@ module ActionDispatch
77
93
 
78
94
  request = Request.new(env)
79
95
 
80
- if authorized?(request)
96
+ if authorized?(request) || excluded?(request)
81
97
  mark_as_authorized(request)
82
98
  @app.call(env)
83
99
  else
@@ -89,7 +105,7 @@ module ActionDispatch
89
105
  def authorized?(request)
90
106
  valid_host = /
91
107
  \A
92
- (?<host>[a-z0-9.-]+|\[[a-f0-9]*:[a-f0-9\.:]+\])
108
+ (?<host>[a-z0-9.-]+|\[[a-f0-9]*:[a-f0-9.:]+\])
93
109
  (:\d+)?
94
110
  \z
95
111
  /x
@@ -103,6 +119,10 @@ module ActionDispatch
103
119
  forwarded_host.nil? || @permissions.allows?(forwarded_host[:host]))
104
120
  end
105
121
 
122
+ def excluded?(request)
123
+ @exclude && @exclude.call(request)
124
+ end
125
+
106
126
  def mark_as_authorized(request)
107
127
  request.set_header("action_dispatch.authorized_host", request.host)
108
128
  end