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.

Files changed (116) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +393 -253
  3. data/MIT-LICENSE +1 -2
  4. data/lib/abstract_controller/base.rb +35 -2
  5. data/lib/abstract_controller/callbacks.rb +2 -2
  6. data/lib/abstract_controller/collector.rb +4 -2
  7. data/lib/abstract_controller/helpers.rb +105 -90
  8. data/lib/abstract_controller/railties/routes_helpers.rb +17 -1
  9. data/lib/abstract_controller/rendering.rb +9 -9
  10. data/lib/abstract_controller/translation.rb +8 -2
  11. data/lib/abstract_controller.rb +1 -0
  12. data/lib/action_controller/api.rb +2 -2
  13. data/lib/action_controller/base.rb +4 -2
  14. data/lib/action_controller/caching.rb +0 -1
  15. data/lib/action_controller/log_subscriber.rb +3 -3
  16. data/lib/action_controller/metal/conditional_get.rb +11 -3
  17. data/lib/action_controller/metal/content_security_policy.rb +1 -1
  18. data/lib/action_controller/metal/cookies.rb +3 -1
  19. data/lib/action_controller/metal/data_streaming.rb +1 -1
  20. data/lib/action_controller/metal/etag_with_template_digest.rb +3 -5
  21. data/lib/action_controller/metal/exceptions.rb +33 -0
  22. data/lib/action_controller/metal/head.rb +7 -4
  23. data/lib/action_controller/metal/helpers.rb +11 -1
  24. data/lib/action_controller/metal/http_authentication.rb +5 -2
  25. data/lib/action_controller/metal/implicit_render.rb +1 -1
  26. data/lib/action_controller/metal/instrumentation.rb +11 -9
  27. data/lib/action_controller/metal/live.rb +10 -1
  28. data/lib/action_controller/metal/logging.rb +20 -0
  29. data/lib/action_controller/metal/mime_responds.rb +6 -2
  30. data/lib/action_controller/metal/parameter_encoding.rb +35 -4
  31. data/lib/action_controller/metal/params_wrapper.rb +14 -8
  32. data/lib/action_controller/metal/permissions_policy.rb +46 -0
  33. data/lib/action_controller/metal/redirecting.rb +1 -1
  34. data/lib/action_controller/metal/rendering.rb +6 -0
  35. data/lib/action_controller/metal/request_forgery_protection.rb +1 -1
  36. data/lib/action_controller/metal/rescue.rb +1 -1
  37. data/lib/action_controller/metal/strong_parameters.rb +104 -16
  38. data/lib/action_controller/metal.rb +2 -2
  39. data/lib/action_controller/renderer.rb +23 -13
  40. data/lib/action_controller/test_case.rb +65 -56
  41. data/lib/action_controller.rb +2 -3
  42. data/lib/action_dispatch/http/cache.rb +18 -17
  43. data/lib/action_dispatch/http/content_security_policy.rb +6 -1
  44. data/lib/action_dispatch/http/filter_parameters.rb +1 -1
  45. data/lib/action_dispatch/http/filter_redirect.rb +1 -1
  46. data/lib/action_dispatch/http/headers.rb +3 -2
  47. data/lib/action_dispatch/http/mime_negotiation.rb +14 -8
  48. data/lib/action_dispatch/http/mime_type.rb +29 -16
  49. data/lib/action_dispatch/http/parameters.rb +1 -19
  50. data/lib/action_dispatch/http/permissions_policy.rb +173 -0
  51. data/lib/action_dispatch/http/request.rb +24 -8
  52. data/lib/action_dispatch/http/response.rb +17 -16
  53. data/lib/action_dispatch/http/url.rb +3 -2
  54. data/lib/action_dispatch/journey/formatter.rb +55 -30
  55. data/lib/action_dispatch/journey/gtg/builder.rb +22 -36
  56. data/lib/action_dispatch/journey/gtg/simulator.rb +8 -7
  57. data/lib/action_dispatch/journey/gtg/transition_table.rb +6 -4
  58. data/lib/action_dispatch/journey/nfa/dot.rb +0 -11
  59. data/lib/action_dispatch/journey/nodes/node.rb +4 -3
  60. data/lib/action_dispatch/journey/parser.rb +13 -13
  61. data/lib/action_dispatch/journey/parser.y +1 -1
  62. data/lib/action_dispatch/journey/path/pattern.rb +13 -18
  63. data/lib/action_dispatch/journey/route.rb +7 -18
  64. data/lib/action_dispatch/journey/router/utils.rb +6 -4
  65. data/lib/action_dispatch/journey/router.rb +26 -30
  66. data/lib/action_dispatch/journey/visitors.rb +1 -1
  67. data/lib/action_dispatch/journey.rb +0 -2
  68. data/lib/action_dispatch/middleware/actionable_exceptions.rb +1 -1
  69. data/lib/action_dispatch/middleware/cookies.rb +89 -46
  70. data/lib/action_dispatch/middleware/debug_exceptions.rb +8 -15
  71. data/lib/action_dispatch/middleware/debug_view.rb +1 -1
  72. data/lib/action_dispatch/middleware/exception_wrapper.rb +28 -16
  73. data/lib/action_dispatch/middleware/host_authorization.rb +63 -14
  74. data/lib/action_dispatch/middleware/remote_ip.rb +5 -4
  75. data/lib/action_dispatch/middleware/request_id.rb +4 -5
  76. data/lib/action_dispatch/middleware/session/abstract_store.rb +2 -2
  77. data/lib/action_dispatch/middleware/session/cookie_store.rb +2 -2
  78. data/lib/action_dispatch/middleware/show_exceptions.rb +12 -0
  79. data/lib/action_dispatch/middleware/ssl.rb +12 -7
  80. data/lib/action_dispatch/middleware/stack.rb +19 -1
  81. data/lib/action_dispatch/middleware/static.rb +154 -93
  82. data/lib/action_dispatch/middleware/templates/rescues/_message_and_suggestions.html.erb +22 -0
  83. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +2 -5
  84. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +2 -2
  85. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +2 -2
  86. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +100 -8
  87. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +1 -1
  88. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +21 -1
  89. data/lib/action_dispatch/railtie.rb +3 -2
  90. data/lib/action_dispatch/request/session.rb +2 -8
  91. data/lib/action_dispatch/request/utils.rb +26 -2
  92. data/lib/action_dispatch/routing/inspector.rb +8 -7
  93. data/lib/action_dispatch/routing/mapper.rb +102 -71
  94. data/lib/action_dispatch/routing/polymorphic_routes.rb +12 -11
  95. data/lib/action_dispatch/routing/redirection.rb +4 -4
  96. data/lib/action_dispatch/routing/route_set.rb +49 -41
  97. data/lib/action_dispatch/system_test_case.rb +35 -24
  98. data/lib/action_dispatch/system_testing/browser.rb +33 -27
  99. data/lib/action_dispatch/system_testing/driver.rb +6 -7
  100. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +47 -6
  101. data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +4 -7
  102. data/lib/action_dispatch/testing/assertions/response.rb +2 -4
  103. data/lib/action_dispatch/testing/assertions/routing.rb +5 -5
  104. data/lib/action_dispatch/testing/assertions.rb +1 -1
  105. data/lib/action_dispatch/testing/integration.rb +40 -29
  106. data/lib/action_dispatch/testing/test_process.rb +32 -4
  107. data/lib/action_dispatch/testing/test_request.rb +3 -3
  108. data/lib/action_dispatch.rb +3 -2
  109. data/lib/action_pack/gem_version.rb +2 -2
  110. data/lib/action_pack.rb +1 -1
  111. metadata +18 -19
  112. data/lib/action_controller/metal/force_ssl.rb +0 -58
  113. data/lib/action_dispatch/http/parameter_filter.rb +0 -12
  114. data/lib/action_dispatch/journey/nfa/builder.rb +0 -78
  115. data/lib/action_dispatch/journey/nfa/simulator.rb +0 -47
  116. 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
@@ -40,7 +40,7 @@ module ActionDispatch
40
40
  @parameters.each do |index|
41
41
  param = parts[index]
42
42
  value = hash[param.name]
43
- return "" unless value
43
+ return "" if value.nil?
44
44
  parts[index] = param.escape value
45
45
  end
46
46
 
@@ -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
@@ -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).tap do |hash|
290
- hash.update(cookies)
291
- end
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
- Marshal
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
- 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,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, which responds with +403 Forbidden+.
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
- DEFAULT_RESPONSE_APP = -> env do
78
- request = Request.new(env)
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
- format = request.xhr? ? "text/plain" : "text/html"
81
- template = DebugView.new(host: request.host)
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
- [403, {
85
- "Content-Type" => "#{format}; charset=#{Response.default_charset}",
86
- "Content-Length" => body.bytesize.to_s,
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, response_app = nil)
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
- @response_app = response_app || DEFAULT_RESPONSE_APP
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.1", # localhost IPv4
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, remote_addr].flatten.compact
146
+ ips = [forwarded_ips, client_ips].flatten.compact
147
147
 
148
- # If every single IP option is in the trusted list, just return REMOTE_ADDR
149
- filter_proxies(ips).first || remote_addr
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
- X_REQUEST_ID = "X-Request-Id" #:nodoc:
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.x_request_id)
27
- @app.call(env).tap { |_status, headers, _body| headers[X_REQUEST_ID] = req.request_id }
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, session_id, cookie)
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, session_id, cookie)
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 4K cookie size limit. A CookieOverflow exception is raised if
14
- # you attempt to store more than 4K of data.
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.