actionpack 6.0.4.1 → 6.1.0.rc1

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 (112) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +241 -304
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +1 -1
  5. data/lib/abstract_controller/base.rb +35 -2
  6. data/lib/abstract_controller/callbacks.rb +2 -2
  7. data/lib/abstract_controller/helpers.rb +105 -90
  8. data/lib/abstract_controller/rendering.rb +9 -9
  9. data/lib/abstract_controller/translation.rb +8 -2
  10. data/lib/abstract_controller.rb +1 -0
  11. data/lib/action_controller/api.rb +2 -2
  12. data/lib/action_controller/base.rb +4 -2
  13. data/lib/action_controller/caching.rb +0 -1
  14. data/lib/action_controller/log_subscriber.rb +3 -3
  15. data/lib/action_controller/metal/conditional_get.rb +10 -2
  16. data/lib/action_controller/metal/content_security_policy.rb +1 -1
  17. data/lib/action_controller/metal/data_streaming.rb +1 -1
  18. data/lib/action_controller/metal/etag_with_template_digest.rb +2 -4
  19. data/lib/action_controller/metal/exceptions.rb +33 -0
  20. data/lib/action_controller/metal/feature_policy.rb +46 -0
  21. data/lib/action_controller/metal/head.rb +7 -4
  22. data/lib/action_controller/metal/helpers.rb +11 -1
  23. data/lib/action_controller/metal/http_authentication.rb +5 -3
  24. data/lib/action_controller/metal/implicit_render.rb +1 -1
  25. data/lib/action_controller/metal/instrumentation.rb +11 -9
  26. data/lib/action_controller/metal/live.rb +1 -1
  27. data/lib/action_controller/metal/logging.rb +20 -0
  28. data/lib/action_controller/metal/mime_responds.rb +6 -2
  29. data/lib/action_controller/metal/parameter_encoding.rb +35 -4
  30. data/lib/action_controller/metal/params_wrapper.rb +16 -11
  31. data/lib/action_controller/metal/redirecting.rb +1 -1
  32. data/lib/action_controller/metal/rendering.rb +6 -0
  33. data/lib/action_controller/metal/request_forgery_protection.rb +1 -1
  34. data/lib/action_controller/metal/rescue.rb +1 -1
  35. data/lib/action_controller/metal/strong_parameters.rb +103 -15
  36. data/lib/action_controller/metal.rb +2 -2
  37. data/lib/action_controller/renderer.rb +24 -13
  38. data/lib/action_controller/test_case.rb +62 -56
  39. data/lib/action_controller.rb +2 -3
  40. data/lib/action_dispatch/http/cache.rb +12 -10
  41. data/lib/action_dispatch/http/content_security_policy.rb +5 -1
  42. data/lib/action_dispatch/http/feature_policy.rb +168 -0
  43. data/lib/action_dispatch/http/filter_parameters.rb +1 -1
  44. data/lib/action_dispatch/http/filter_redirect.rb +1 -1
  45. data/lib/action_dispatch/http/headers.rb +3 -2
  46. data/lib/action_dispatch/http/mime_negotiation.rb +14 -8
  47. data/lib/action_dispatch/http/mime_type.rb +29 -16
  48. data/lib/action_dispatch/http/parameters.rb +1 -19
  49. data/lib/action_dispatch/http/request.rb +24 -8
  50. data/lib/action_dispatch/http/response.rb +17 -16
  51. data/lib/action_dispatch/http/url.rb +3 -2
  52. data/lib/action_dispatch/journey/formatter.rb +53 -28
  53. data/lib/action_dispatch/journey/gtg/builder.rb +22 -36
  54. data/lib/action_dispatch/journey/gtg/simulator.rb +8 -7
  55. data/lib/action_dispatch/journey/gtg/transition_table.rb +6 -4
  56. data/lib/action_dispatch/journey/nfa/dot.rb +0 -11
  57. data/lib/action_dispatch/journey/nodes/node.rb +4 -3
  58. data/lib/action_dispatch/journey/parser.rb +13 -13
  59. data/lib/action_dispatch/journey/parser.y +1 -1
  60. data/lib/action_dispatch/journey/path/pattern.rb +13 -18
  61. data/lib/action_dispatch/journey/route.rb +7 -18
  62. data/lib/action_dispatch/journey/router/utils.rb +6 -4
  63. data/lib/action_dispatch/journey/router.rb +26 -30
  64. data/lib/action_dispatch/journey.rb +0 -2
  65. data/lib/action_dispatch/middleware/actionable_exceptions.rb +1 -1
  66. data/lib/action_dispatch/middleware/cookies.rb +67 -32
  67. data/lib/action_dispatch/middleware/debug_exceptions.rb +8 -15
  68. data/lib/action_dispatch/middleware/debug_view.rb +1 -1
  69. data/lib/action_dispatch/middleware/exception_wrapper.rb +28 -16
  70. data/lib/action_dispatch/middleware/host_authorization.rb +29 -12
  71. data/lib/action_dispatch/middleware/remote_ip.rb +5 -4
  72. data/lib/action_dispatch/middleware/request_id.rb +4 -5
  73. data/lib/action_dispatch/middleware/session/abstract_store.rb +2 -2
  74. data/lib/action_dispatch/middleware/session/cookie_store.rb +2 -2
  75. data/lib/action_dispatch/middleware/ssl.rb +9 -6
  76. data/lib/action_dispatch/middleware/stack.rb +18 -0
  77. data/lib/action_dispatch/middleware/static.rb +154 -93
  78. data/lib/action_dispatch/middleware/templates/rescues/_message_and_suggestions.html.erb +18 -0
  79. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +2 -5
  80. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +2 -2
  81. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +2 -3
  82. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +88 -8
  83. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +1 -1
  84. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +12 -1
  85. data/lib/action_dispatch/railtie.rb +3 -2
  86. data/lib/action_dispatch/request/session.rb +2 -8
  87. data/lib/action_dispatch/request/utils.rb +26 -2
  88. data/lib/action_dispatch/routing/inspector.rb +8 -7
  89. data/lib/action_dispatch/routing/mapper.rb +102 -71
  90. data/lib/action_dispatch/routing/polymorphic_routes.rb +16 -19
  91. data/lib/action_dispatch/routing/redirection.rb +3 -3
  92. data/lib/action_dispatch/routing/route_set.rb +49 -41
  93. data/lib/action_dispatch/system_test_case.rb +29 -24
  94. data/lib/action_dispatch/system_testing/browser.rb +33 -27
  95. data/lib/action_dispatch/system_testing/driver.rb +6 -7
  96. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +47 -6
  97. data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +4 -7
  98. data/lib/action_dispatch/testing/assertions/response.rb +2 -4
  99. data/lib/action_dispatch/testing/assertions/routing.rb +5 -5
  100. data/lib/action_dispatch/testing/assertions.rb +1 -1
  101. data/lib/action_dispatch/testing/integration.rb +38 -27
  102. data/lib/action_dispatch/testing/test_process.rb +29 -4
  103. data/lib/action_dispatch/testing/test_request.rb +3 -3
  104. data/lib/action_dispatch.rb +3 -2
  105. data/lib/action_pack/gem_version.rb +3 -3
  106. data/lib/action_pack.rb +1 -1
  107. metadata +23 -24
  108. data/lib/action_controller/metal/force_ssl.rb +0 -58
  109. data/lib/action_dispatch/http/parameter_filter.rb +0 -12
  110. data/lib/action_dispatch/journey/nfa/builder.rb +0 -78
  111. data/lib/action_dispatch/journey/nfa/simulator.rb +0 -47
  112. 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
- parameters = route.defaults.merge parameters.transform_values { |val|
44
- val.dup.force_encoding(::Encoding::UTF_8)
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 = set_params.merge 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.sub(/^([^\/])/, '/\1')
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
- routes = filter_routes(req.path_info).concat custom_routes.find_all { |r|
109
- r.path.match(req.path_info)
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
- routes =
113
- if req.head?
114
- match_head_routes(routes, req)
115
- else
116
- match_routes(routes, req)
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(req.path_info)
124
+ match_data = r.path.match(path_info)
123
125
  path_parameters = {}
124
- match_data.names.zip(match_data.captures) { |name, val|
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
- verb_specific_routes = routes.select(&:requires_matching_verb?)
133
- head_routes = match_routes(verb_specific_routes, req)
134
-
135
- if head_routes.empty?
136
- begin
137
- req.request_method = "GET"
138
- match_routes(routes, req)
139
- ensure
140
- req.request_method = "HEAD"
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
@@ -3,5 +3,3 @@
3
3
  require "action_dispatch/journey/router"
4
4
  require "action_dispatch/journey/gtg/builder"
5
5
  require "action_dispatch/journey/gtg/simulator"
6
- require "action_dispatch/journey/nfa/builder"
7
- require "action_dispatch/journey/nfa/simulator"
@@ -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)
@@ -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:
@@ -508,6 +526,18 @@ module ActionDispatch
508
526
  end
509
527
  end
510
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
+
511
541
  class JsonSerializer # :nodoc:
512
542
  def self.load(value)
513
543
  ActiveSupport::JSON.decode(value)
@@ -555,7 +585,7 @@ module ActionDispatch
555
585
  serializer = request.cookies_serializer || :marshal
556
586
  case serializer
557
587
  when :marshal
558
- Marshal
588
+ MarshalWithJsonFallback
559
589
  when :json, :hybrid
560
590
  JsonSerializer
561
591
  else
@@ -625,6 +655,11 @@ module ActionDispatch
625
655
  sign_secret = request.key_generator.generate_key(request.encrypted_signed_cookie_salt)
626
656
 
627
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)
628
663
  end
629
664
  end
630
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
@@ -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,
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" => :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
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,7 +4,12 @@ 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
@@ -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/i
54
+ /\A(.+\.)?#{Regexp.escape(host[1..-1])}\z/
50
55
  else
51
- /\A#{Regexp.escape host}\z/i
56
+ host
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
@@ -86,15 +102,16 @@ module ActionDispatch
86
102
  end
87
103
 
88
104
  private
89
- HOSTNAME = /[a-z0-9.-]+|\[[a-f0-9]*:[a-f0-9.:]+\]/i
90
- VALID_ORIGIN_HOST = /\A(#{HOSTNAME})(?::\d+)?\z/
91
- VALID_FORWARDED_HOST = /(?:\A|,[ ]?)(#{HOSTNAME})(?::\d+)?\z/
92
-
93
105
  def authorized?(request)
94
- origin_host = request.get_header("HTTP_HOST")&.slice(VALID_ORIGIN_HOST, 1) || ""
95
- forwarded_host = request.x_forwarded_host&.slice(VALID_FORWARDED_HOST, 1) || ""
106
+ origin_host = request.get_header("HTTP_HOST").to_s.sub(/:\d+\z/, "")
107
+ forwarded_host = request.x_forwarded_host.to_s.split(/,\s?/).last.to_s.sub(/:\d+\z/, "")
108
+
109
+ @permissions.allows?(origin_host) &&
110
+ (forwarded_host.blank? || @permissions.allows?(forwarded_host))
111
+ end
96
112
 
97
- @permissions.allows?(origin_host) && (forwarded_host.blank? || @permissions.allows?(forwarded_host))
113
+ def excluded?(request)
114
+ @exclude && @exclude.call(request)
98
115
  end
99
116
 
100
117
  def mark_as_authorized(request)