actionpack 6.0.6.1 → 6.1.7.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (116) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +416 -255
  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 +21 -2
  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 +3 -3
  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}\z/.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.