actionpack 7.0.8.1 → 7.1.0.beta1

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 (135) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +320 -393
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +2 -2
  5. data/lib/abstract_controller/base.rb +19 -10
  6. data/lib/abstract_controller/caching/fragments.rb +2 -0
  7. data/lib/abstract_controller/callbacks.rb +31 -6
  8. data/lib/abstract_controller/deprecator.rb +7 -0
  9. data/lib/abstract_controller/helpers.rb +61 -18
  10. data/lib/abstract_controller/railties/routes_helpers.rb +1 -16
  11. data/lib/abstract_controller/rendering.rb +3 -3
  12. data/lib/abstract_controller/translation.rb +1 -27
  13. data/lib/abstract_controller/url_for.rb +2 -0
  14. data/lib/abstract_controller.rb +6 -0
  15. data/lib/action_controller/api.rb +5 -3
  16. data/lib/action_controller/base.rb +3 -17
  17. data/lib/action_controller/caching.rb +2 -0
  18. data/lib/action_controller/deprecator.rb +7 -0
  19. data/lib/action_controller/form_builder.rb +2 -0
  20. data/lib/action_controller/log_subscriber.rb +16 -4
  21. data/lib/action_controller/metal/content_security_policy.rb +1 -1
  22. data/lib/action_controller/metal/data_streaming.rb +2 -0
  23. data/lib/action_controller/metal/default_headers.rb +2 -0
  24. data/lib/action_controller/metal/etag_with_flash.rb +2 -0
  25. data/lib/action_controller/metal/etag_with_template_digest.rb +2 -0
  26. data/lib/action_controller/metal/exceptions.rb +8 -0
  27. data/lib/action_controller/metal/head.rb +8 -6
  28. data/lib/action_controller/metal/helpers.rb +3 -14
  29. data/lib/action_controller/metal/http_authentication.rb +11 -4
  30. data/lib/action_controller/metal/implicit_render.rb +5 -3
  31. data/lib/action_controller/metal/instrumentation.rb +8 -1
  32. data/lib/action_controller/metal/live.rb +24 -0
  33. data/lib/action_controller/metal/mime_responds.rb +2 -2
  34. data/lib/action_controller/metal/params_wrapper.rb +3 -1
  35. data/lib/action_controller/metal/permissions_policy.rb +1 -1
  36. data/lib/action_controller/metal/redirecting.rb +6 -6
  37. data/lib/action_controller/metal/renderers.rb +2 -2
  38. data/lib/action_controller/metal/rendering.rb +0 -7
  39. data/lib/action_controller/metal/request_forgery_protection.rb +138 -50
  40. data/lib/action_controller/metal/rescue.rb +2 -0
  41. data/lib/action_controller/metal/streaming.rb +70 -30
  42. data/lib/action_controller/metal/strong_parameters.rb +89 -50
  43. data/lib/action_controller/metal/url_for.rb +7 -0
  44. data/lib/action_controller/metal.rb +79 -21
  45. data/lib/action_controller/railtie.rb +22 -9
  46. data/lib/action_controller/renderer.rb +98 -65
  47. data/lib/action_controller/test_case.rb +15 -5
  48. data/lib/action_controller.rb +8 -1
  49. data/lib/action_dispatch/constants.rb +32 -0
  50. data/lib/action_dispatch/deprecator.rb +7 -0
  51. data/lib/action_dispatch/http/cache.rb +1 -3
  52. data/lib/action_dispatch/http/content_security_policy.rb +9 -8
  53. data/lib/action_dispatch/http/filter_parameters.rb +11 -5
  54. data/lib/action_dispatch/http/headers.rb +2 -0
  55. data/lib/action_dispatch/http/mime_negotiation.rb +21 -21
  56. data/lib/action_dispatch/http/mime_type.rb +35 -12
  57. data/lib/action_dispatch/http/mime_types.rb +3 -1
  58. data/lib/action_dispatch/http/parameters.rb +1 -1
  59. data/lib/action_dispatch/http/permissions_policy.rb +39 -17
  60. data/lib/action_dispatch/http/rack_cache.rb +2 -0
  61. data/lib/action_dispatch/http/request.rb +48 -14
  62. data/lib/action_dispatch/http/response.rb +78 -59
  63. data/lib/action_dispatch/http/upload.rb +2 -0
  64. data/lib/action_dispatch/journey/formatter.rb +8 -2
  65. data/lib/action_dispatch/journey/path/pattern.rb +14 -14
  66. data/lib/action_dispatch/journey/route.rb +3 -2
  67. data/lib/action_dispatch/journey/router.rb +5 -4
  68. data/lib/action_dispatch/journey/routes.rb +2 -2
  69. data/lib/action_dispatch/log_subscriber.rb +23 -0
  70. data/lib/action_dispatch/middleware/actionable_exceptions.rb +5 -6
  71. data/lib/action_dispatch/middleware/assume_ssl.rb +24 -0
  72. data/lib/action_dispatch/middleware/callbacks.rb +2 -0
  73. data/lib/action_dispatch/middleware/cookies.rb +81 -98
  74. data/lib/action_dispatch/middleware/debug_exceptions.rb +26 -25
  75. data/lib/action_dispatch/middleware/debug_locks.rb +4 -1
  76. data/lib/action_dispatch/middleware/debug_view.rb +7 -2
  77. data/lib/action_dispatch/middleware/exception_wrapper.rb +181 -27
  78. data/lib/action_dispatch/middleware/executor.rb +1 -1
  79. data/lib/action_dispatch/middleware/flash.rb +7 -0
  80. data/lib/action_dispatch/middleware/host_authorization.rb +6 -3
  81. data/lib/action_dispatch/middleware/public_exceptions.rb +5 -3
  82. data/lib/action_dispatch/middleware/reloader.rb +7 -5
  83. data/lib/action_dispatch/middleware/remote_ip.rb +17 -16
  84. data/lib/action_dispatch/middleware/request_id.rb +2 -0
  85. data/lib/action_dispatch/middleware/server_timing.rb +4 -4
  86. data/lib/action_dispatch/middleware/session/abstract_store.rb +5 -0
  87. data/lib/action_dispatch/middleware/session/cache_store.rb +2 -0
  88. data/lib/action_dispatch/middleware/session/cookie_store.rb +11 -5
  89. data/lib/action_dispatch/middleware/session/mem_cache_store.rb +3 -1
  90. data/lib/action_dispatch/middleware/show_exceptions.rb +19 -15
  91. data/lib/action_dispatch/middleware/ssl.rb +18 -6
  92. data/lib/action_dispatch/middleware/stack.rb +7 -2
  93. data/lib/action_dispatch/middleware/static.rb +12 -8
  94. data/lib/action_dispatch/middleware/templates/rescues/_actions.html.erb +2 -2
  95. data/lib/action_dispatch/middleware/templates/rescues/_message_and_suggestions.html.erb +4 -4
  96. data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +8 -1
  97. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +7 -7
  98. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb +2 -2
  99. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +17 -0
  100. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb +16 -12
  101. data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +1 -1
  102. data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +3 -3
  103. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +4 -4
  104. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +1 -1
  105. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.text.erb +1 -1
  106. data/lib/action_dispatch/middleware/templates/routes/_route.html.erb +3 -0
  107. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +46 -37
  108. data/lib/action_dispatch/railtie.rb +14 -4
  109. data/lib/action_dispatch/request/session.rb +16 -6
  110. data/lib/action_dispatch/request/utils.rb +8 -3
  111. data/lib/action_dispatch/routing/inspector.rb +54 -6
  112. data/lib/action_dispatch/routing/mapper.rb +26 -14
  113. data/lib/action_dispatch/routing/polymorphic_routes.rb +2 -0
  114. data/lib/action_dispatch/routing/redirection.rb +15 -6
  115. data/lib/action_dispatch/routing/route_set.rb +52 -22
  116. data/lib/action_dispatch/routing/routes_proxy.rb +1 -1
  117. data/lib/action_dispatch/routing/url_for.rb +5 -1
  118. data/lib/action_dispatch/routing.rb +4 -4
  119. data/lib/action_dispatch/system_test_case.rb +3 -3
  120. data/lib/action_dispatch/system_testing/browser.rb +5 -6
  121. data/lib/action_dispatch/system_testing/driver.rb +13 -21
  122. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +27 -16
  123. data/lib/action_dispatch/testing/assertions/response.rb +13 -6
  124. data/lib/action_dispatch/testing/assertions/routing.rb +67 -28
  125. data/lib/action_dispatch/testing/assertions.rb +3 -1
  126. data/lib/action_dispatch/testing/integration.rb +27 -17
  127. data/lib/action_dispatch/testing/request_encoder.rb +4 -1
  128. data/lib/action_dispatch/testing/test_process.rb +4 -3
  129. data/lib/action_dispatch/testing/test_request.rb +1 -1
  130. data/lib/action_dispatch/testing/test_response.rb +23 -9
  131. data/lib/action_dispatch.rb +37 -4
  132. data/lib/action_pack/gem_version.rb +4 -4
  133. data/lib/action_pack/version.rb +1 -1
  134. data/lib/action_pack.rb +1 -1
  135. metadata +52 -30
@@ -18,18 +18,20 @@ module ActionController
18
18
  # render
19
19
  #
20
20
  # See +Rack::Utils::SYMBOL_TO_STATUS_CODE+ for a full list of valid +status+ symbols.
21
- def head(status, options = {})
21
+ def head(status, options = nil)
22
22
  if status.is_a?(Hash)
23
23
  raise ArgumentError, "#{status.inspect} is not a valid value for `status`."
24
24
  end
25
25
 
26
26
  status ||= :ok
27
27
 
28
- location = options.delete(:location)
29
- content_type = options.delete(:content_type)
28
+ if options
29
+ location = options.delete(:location)
30
+ content_type = options.delete(:content_type)
30
31
 
31
- options.each do |key, value|
32
- headers[key.to_s.split(/[-_]/).each { |v| v[0] = v[0].upcase }.join("-")] = value.to_s
32
+ options.each do |key, value|
33
+ headers[key.to_s.split(/[-_]/).each { |v| v[0] = v[0].upcase }.join("-")] = value.to_s
34
+ end
33
35
  end
34
36
 
35
37
  self.status = status
@@ -37,7 +39,7 @@ module ActionController
37
39
 
38
40
  if include_content?(response_code)
39
41
  unless self.media_type
40
- self.content_type = content_type || (Mime[formats.first] if formats) || Mime[:html]
42
+ self.content_type = content_type || ((f = formats) && Mime[f.first]) || Mime[:html]
41
43
  end
42
44
 
43
45
  response.charset = false
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActionController
4
+ # = Action Controller \Helpers
5
+ #
4
6
  # The \Rails framework provides a large number of helpers for working with assets, dates, forms,
5
7
  # numbers and model objects, to name a few. These helpers are available to all templates
6
8
  # by default.
@@ -80,7 +82,7 @@ module ActionController
80
82
  # Provides a proxy to access helper methods from outside the view.
81
83
  #
82
84
  # Note that the proxy is rendered under a different view context.
83
- # This may cause incorrect behaviour with capture methods. Consider
85
+ # This may cause incorrect behavior with capture methods. Consider
84
86
  # using {helper}[rdoc-ref:AbstractController::Helpers::ClassMethods#helper]
85
87
  # instead when using +capture+.
86
88
  def helpers
@@ -104,19 +106,6 @@ module ActionController
104
106
  super(args)
105
107
  end
106
108
 
107
- # Returns a list of helper names in a given path.
108
- #
109
- # ActionController::Base.all_helpers_from_path 'app/helpers'
110
- # # => ["application", "chart", "rubygems"]
111
- def all_helpers_from_path(path)
112
- helpers = Array(path).flat_map do |_path|
113
- names = Dir["#{_path}/**/*_helper.rb"].map { |file| file[_path.to_s.size + 1..-"_helper.rb".size - 1] }
114
- names.sort!
115
- end
116
- helpers.uniq!
117
- helpers
118
- end
119
-
120
109
  private
121
110
  # Extract helper names from files in <tt>app/helpers/**/*_helper.rb</tt>
122
111
  def all_application_helpers
@@ -316,7 +316,7 @@ module ActionController
316
316
  # of this document.
317
317
  #
318
318
  # The nonce is opaque to the client. Composed of Time, and hash of Time with secret
319
- # key from the Rails session secret generated upon creation of project. Ensures
319
+ # key from the \Rails session secret generated upon creation of project. Ensures
320
320
  # the time cannot be modified by client.
321
321
  def nonce(secret_key, time = Time.now)
322
322
  t = time.to_i
@@ -431,8 +431,11 @@ module ActionController
431
431
  authenticate_with_http_token(&login_procedure) || request_http_token_authentication(realm, message)
432
432
  end
433
433
 
434
- # Authenticate using an HTTP Bearer token. Returns true if
435
- # authentication is successful, false otherwise.
434
+ # Authenticate using an HTTP Bearer token.
435
+ # Returns the return value of <tt>login_procedure</tt> if a
436
+ # token is found. Returns <tt>nil</tt> if no token is found.
437
+ #
438
+ # See ActionController::HttpAuthentication::Token for example usage.
436
439
  def authenticate_with_http_token(&login_procedure)
437
440
  Token.authenticate(self, &login_procedure)
438
441
  end
@@ -502,11 +505,15 @@ module ActionController
502
505
  array_params.each { |param| (param[1] || +"").gsub! %r/^"|"$/, "" }
503
506
  end
504
507
 
508
+ WHITESPACED_AUTHN_PAIR_DELIMITERS = /\s*#{AUTHN_PAIR_DELIMITERS}\s*/
509
+ private_constant :WHITESPACED_AUTHN_PAIR_DELIMITERS
510
+
505
511
  # This method takes an authorization body and splits up the key-value
506
512
  # pairs by the standardized <tt>:</tt>, <tt>;</tt>, or <tt>\t</tt>
507
513
  # delimiters defined in +AUTHN_PAIR_DELIMITERS+.
508
514
  def raw_params(auth)
509
- _raw_params = auth.sub(TOKEN_REGEX, "").split(/\s*#{AUTHN_PAIR_DELIMITERS}\s*/)
515
+ _raw_params = auth.sub(TOKEN_REGEX, "").split(WHITESPACED_AUTHN_PAIR_DELIMITERS)
516
+ _raw_params.reject!(&:empty?)
510
517
 
511
518
  if !_raw_params.first&.start_with?(TOKEN_KEY)
512
519
  _raw_params[0] = "#{TOKEN_KEY}#{_raw_params.first}"
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActionController
4
+ # = Action Controller Implicit Render
5
+ #
4
6
  # Handles implicit rendering for a controller action that does not
5
7
  # explicitly respond with +render+, +respond_to+, +redirect+, or +head+.
6
8
  #
@@ -17,12 +19,12 @@ module ActionController
17
19
  # Second, if we DON'T find a template but the controller action does have
18
20
  # templates for other formats, variants, etc., then we trust that you meant
19
21
  # to provide a template for this response, too, and we raise
20
- # <tt>ActionController::UnknownFormat</tt> with an explanation.
22
+ # ActionController::UnknownFormat with an explanation.
21
23
  #
22
24
  # Third, if we DON'T find a template AND the request is a page load in a web
23
25
  # browser (technically, a non-XHR GET request for an HTML response) where
24
26
  # you reasonably expect to have rendered a template, then we raise
25
- # <tt>ActionController::MissingExactTemplate</tt> with an explanation.
27
+ # ActionController::MissingExactTemplate with an explanation.
26
28
  #
27
29
  # Finally, if we DON'T find a template AND the request isn't a browser page
28
30
  # load, then we implicitly respond with <tt>204 No Content</tt>.
@@ -42,7 +44,7 @@ module ActionController
42
44
  raise ActionController::UnknownFormat, message
43
45
  elsif interactive_browser_request?
44
46
  message = "#{self.class.name}\##{action_name} is missing a template for request formats: #{request.formats.map(&:to_s).join(',')}"
45
- raise ActionController::MissingExactTemplate, message
47
+ raise ActionController::MissingExactTemplate.new(message, self.class, action_name)
46
48
  else
47
49
  logger.info "No template found for #{self.class.name}\##{action_name}, rendering head :no_content" if logger
48
50
  super
@@ -4,6 +4,8 @@ require "benchmark"
4
4
  require "abstract_controller/logger"
5
5
 
6
6
  module ActionController
7
+ # = Action Controller \Instrumentation
8
+ #
7
9
  # Adds instrumentation to several ends in ActionController::Base. It also provides
8
10
  # some hooks related with process_action. This allows an ORM like Active Record
9
11
  # and/or DataMapper to plug in ActionController and show related information.
@@ -16,6 +18,11 @@ module ActionController
16
18
 
17
19
  attr_internal :view_runtime
18
20
 
21
+ def initialize(...) # :nodoc:
22
+ super
23
+ self.view_runtime = nil
24
+ end
25
+
19
26
  def render(*)
20
27
  render_output = nil
21
28
  self.view_runtime = cleanup_view_runtime do
@@ -58,7 +65,7 @@ module ActionController
58
65
  headers: request.headers,
59
66
  format: request.format.ref,
60
67
  method: request.request_method,
61
- path: request.fullpath
68
+ path: request.filtered_path
62
69
  }
63
70
 
64
71
  ActiveSupport::Notifications.instrument("start_processing.action_controller", raw_payload)
@@ -5,6 +5,8 @@ require "delegate"
5
5
  require "active_support/json"
6
6
 
7
7
  module ActionController
8
+ # = Action Controller \Live
9
+ #
8
10
  # Mix this module into your controller, and all actions in that controller
9
11
  # will be able to stream data to the client as it's written.
10
12
  #
@@ -34,6 +36,21 @@ module ActionController
34
36
  # The final caveat is that your actions are executed in a separate thread than
35
37
  # the main thread. Make sure your actions are thread safe, and this shouldn't
36
38
  # be a problem (don't share state across threads, etc).
39
+ #
40
+ # Note that \Rails includes +Rack::ETag+ by default, which will buffer your
41
+ # response. As a result, streaming responses may not work properly with Rack
42
+ # 2.2.x, and you may need to implement workarounds in your application.
43
+ # You can either set the +ETag+ or +Last-Modified+ response headers or remove
44
+ # +Rack::ETag+ from the middleware stack to address this issue.
45
+ #
46
+ # Here's an example of how you can set the +Last-Modified+ header if your Rack
47
+ # version is 2.2.x:
48
+ #
49
+ # def stream
50
+ # response.headers["Content-Type"] = "text/event-stream"
51
+ # response.headers["Last-Modified"] = Time.now.httpdate # Add this line if your Rack version is 2.2.x
52
+ # ...
53
+ # end
37
54
  module Live
38
55
  extend ActiveSupport::Concern
39
56
 
@@ -49,6 +66,8 @@ module ActionController
49
66
  end
50
67
  end
51
68
 
69
+ # = Action Controller \Live Server Sent Events
70
+ #
52
71
  # This class provides the ability to write an SSE (Server Sent Event)
53
72
  # to an IO stream. The class is initialized with a stream and can be used
54
73
  # to either write a JSON string or an object which can be converted to JSON.
@@ -148,6 +167,11 @@ module ActionController
148
167
  @ignore_disconnect = false
149
168
  end
150
169
 
170
+ # ActionDispatch::Response delegates #to_ary to the internal ActionDispatch::Response::Buffer,
171
+ # defining #to_ary is an indicator that the response body can be buffered and/or cached by
172
+ # Rack middlewares, this is not the case for Live responses so we undefine it for this Buffer subclass.
173
+ undef_method :to_ary
174
+
151
175
  def write(string)
152
176
  unless @response.committed?
153
177
  @response.headers["Cache-Control"] ||= "no-cache"
@@ -32,7 +32,7 @@ module ActionController # :nodoc:
32
32
  #
33
33
  # What that says is, "if the client wants HTML or JS in response to this action, just respond as we
34
34
  # would have before, but if the client wants XML, return them the list of people in XML format."
35
- # (Rails determines the desired response format from the HTTP Accept header submitted by the client.)
35
+ # (\Rails determines the desired response format from the HTTP Accept header submitted by the client.)
36
36
  #
37
37
  # Supposing you have an action that adds a new person, optionally creating their company
38
38
  # (by name) if it does not already exist, without web-services, it might look like this:
@@ -98,7 +98,7 @@ module ActionController # :nodoc:
98
98
  #
99
99
  # Note that you can define your own XML parameter parser which would allow you to describe multiple entities
100
100
  # in a single request (i.e., by wrapping them all in a single root node), but if you just go with the flow
101
- # and accept Rails' defaults, life will be much easier.
101
+ # and accept \Rails' defaults, life will be much easier.
102
102
  #
103
103
  # If you need to use a MIME type which isn't supported by default, you can register your own handlers in
104
104
  # +config/initializers/mime_types.rb+ as follows.
@@ -6,6 +6,8 @@ require "active_support/core_ext/module/anonymous"
6
6
  require "action_dispatch/http/mime_type"
7
7
 
8
8
  module ActionController
9
+ # = Action Controller Params Wrapper
10
+ #
9
11
  # Wraps the parameters hash into a nested hash. This will allow clients to
10
12
  # submit requests without having to specify any root elements.
11
13
  #
@@ -68,7 +70,7 @@ module ActionController
68
70
  # class Admin::UsersController < ApplicationController
69
71
  # end
70
72
  #
71
- # will try to check if <tt>Admin::User</tt> or +User+ model exists, and use it to
73
+ # will try to check if +Admin::User+ or +User+ model exists, and use it to
72
74
  # determine the wrapper key respectively. If both models don't exist,
73
75
  # it will then fallback to use +user+ as the key.
74
76
  #
@@ -27,7 +27,7 @@ module ActionController # :nodoc:
27
27
  before_action(options) do
28
28
  if block_given?
29
29
  policy = request.permissions_policy.clone
30
- yield policy
30
+ instance_exec(policy, &block)
31
31
  request.permissions_policy = policy
32
32
  end
33
33
  end
@@ -4,13 +4,13 @@ module ActionController
4
4
  module Redirecting
5
5
  extend ActiveSupport::Concern
6
6
 
7
- ILLEGAL_HEADER_VALUE_REGEX = /[\x00-\x08\x0A-\x1F]/.freeze
8
-
9
7
  include AbstractController::Logger
10
8
  include ActionController::UrlFor
11
9
 
12
10
  class UnsafeRedirectError < StandardError; end
13
11
 
12
+ ILLEGAL_HEADER_VALUE_REGEX = /[\x00-\x08\x0A-\x1F]/.freeze
13
+
14
14
  included do
15
15
  mattr_accessor :raise_on_open_redirects, default: false
16
16
  end
@@ -23,7 +23,7 @@ module ActionController
23
23
  # * <tt>String</tt> not containing a protocol - The current protocol and host is prepended to the string.
24
24
  # * <tt>Proc</tt> - A block that will be executed in the controller's context. Should return any option accepted by +redirect_to+.
25
25
  #
26
- # === Examples:
26
+ # === Examples
27
27
  #
28
28
  # redirect_to action: "show", id: 5
29
29
  # redirect_to @post
@@ -67,8 +67,8 @@ module ActionController
67
67
  #
68
68
  # === Open Redirect protection
69
69
  #
70
- # By default, Rails protects against redirecting to external hosts for your app's safety, so called open redirects.
71
- # Note: this was a new default in Rails 7.0, after upgrading opt-in by uncommenting the line with +raise_on_open_redirects+ in <tt>config/initializers/new_framework_defaults_7_0.rb</tt>
70
+ # By default, \Rails protects against redirecting to external hosts for your app's safety, so called open redirects.
71
+ # Note: this was a new default in \Rails 7.0, after upgrading opt-in by uncommenting the line with +raise_on_open_redirects+ in <tt>config/initializers/new_framework_defaults_7_0.rb</tt>
72
72
  #
73
73
  # Here #redirect_to automatically validates the potentially-unsafe URL:
74
74
  #
@@ -93,7 +93,7 @@ module ActionController
93
93
  _ensure_url_is_http_header_safe(redirect_to_location)
94
94
 
95
95
  self.location = _enforce_open_redirect_protection(redirect_to_location, allow_other_host: allow_other_host)
96
- self.response_body = "<html><body>You are being <a href=\"#{ERB::Util.unwrapped_html_escape(response.location)}\">redirected</a>.</body></html>"
96
+ self.response_body = ""
97
97
  end
98
98
 
99
99
  # Soft deprecated alias for #redirect_back_or_to where the +fallback_location+ location is supplied as a keyword argument instead
@@ -58,7 +58,7 @@ module ActionController
58
58
  # disposition: "attachment; filename=#{filename}.csv"
59
59
  # end
60
60
  #
61
- # Note that we used Mime[:csv] for the csv mime type as it comes with Rails.
61
+ # Note that we used Mime[:csv] for the csv mime type as it comes with \Rails.
62
62
  # For a custom renderer, you'll need to register a mime type with
63
63
  # <tt>Mime::Type.register</tt>.
64
64
  #
@@ -100,7 +100,7 @@ module ActionController
100
100
  #
101
101
  # Both ActionController::Base and ActionController::API
102
102
  # include ActionController::Renderers::All, making all renderers
103
- # available in the controller. See <tt>Renderers::RENDERERS</tt> and <tt>Renderers.add</tt>.
103
+ # available in the controller. See Renderers::RENDERERS and Renderers.add.
104
104
  #
105
105
  # Since ActionController::Metal controllers cannot render, the controller
106
106
  # must include AbstractController::Rendering, ActionController::Rendering,
@@ -195,13 +195,6 @@ module ActionController
195
195
  end
196
196
  end
197
197
 
198
- # Normalize arguments by catching blocks and setting them on :update.
199
- def _normalize_args(action = nil, options = {}, &blk)
200
- options = super
201
- options[:update] = blk if block_given?
202
- options
203
- end
204
-
205
198
  # Normalize both text and status options.
206
199
  def _normalize_options(options)
207
200
  _normalize_text(options)
@@ -11,6 +11,8 @@ module ActionController # :nodoc:
11
11
  class InvalidCrossOriginRequest < ActionControllerError # :nodoc:
12
12
  end
13
13
 
14
+ # = Action Controller Request Forgery Protection
15
+ #
14
16
  # Controller actions are protected from Cross-Site Request Forgery (CSRF) attacks
15
17
  # by including a token in the rendered HTML for your application. This token is
16
18
  # stored as a random string in the session, to which an attacker does not have
@@ -34,10 +36,10 @@ module ActionController # :nodoc:
34
36
  #
35
37
  # Subclasses of ActionController::Base are protected by default with the
36
38
  # <tt>:exception</tt> strategy, which raises an
37
- # <tt>ActionController::InvalidAuthenticityToken</tt> error on unverified requests.
39
+ # ActionController::InvalidAuthenticityToken error on unverified requests.
38
40
  #
39
41
  # APIs may want to disable this behavior since they are typically designed to be
40
- # state-less: that is, the request API client handles the session instead of Rails.
42
+ # state-less: that is, the request API client handles the session instead of \Rails.
41
43
  # One way to achieve this is to use the <tt>:null_session</tt> strategy instead,
42
44
  # which allows unverified requests to be handled, but with an empty session:
43
45
  #
@@ -55,6 +57,8 @@ module ActionController # :nodoc:
55
57
  # Learn more about CSRF attacks and securing your application in the
56
58
  # {Ruby on Rails Security Guide}[https://guides.rubyonrails.org/security.html].
57
59
  module RequestForgeryProtection
60
+ CSRF_TOKEN = "action_controller.csrf_token"
61
+
58
62
  extend ActiveSupport::Concern
59
63
 
60
64
  include AbstractController::Helpers
@@ -90,18 +94,9 @@ module ActionController # :nodoc:
90
94
  config_accessor :default_protect_from_forgery
91
95
  self.default_protect_from_forgery = false
92
96
 
93
- # Controls whether URL-safe CSRF tokens are generated.
94
- config_accessor :urlsafe_csrf_tokens, instance_writer: false
95
- self.urlsafe_csrf_tokens = true
96
-
97
- singleton_class.redefine_method(:urlsafe_csrf_tokens=) do |urlsafe_csrf_tokens|
98
- if urlsafe_csrf_tokens
99
- ActiveSupport::Deprecation.warn("URL-safe CSRF tokens are now the default. Use 6.1 defaults or above.")
100
- else
101
- ActiveSupport::Deprecation.warn("Non-URL-safe CSRF tokens are deprecated. Use 6.1 defaults or above.")
102
- end
103
- config.urlsafe_csrf_tokens = urlsafe_csrf_tokens
104
- end
97
+ # The strategy to use for storing and retrieving CSRF tokens.
98
+ config_accessor :csrf_token_storage_strategy
99
+ self.csrf_token_storage_strategy = SessionStore.new
105
100
 
106
101
  helper_method :form_authenticity_token
107
102
  helper_method :protect_against_forgery?
@@ -148,18 +143,46 @@ module ActionController # :nodoc:
148
143
  # end
149
144
  #
150
145
  # def handle_unverified_request
151
- # # Custom behaviour for unverfied request
146
+ # # Custom behavior for unverfied request
152
147
  # end
153
148
  # end
154
149
  #
155
- # class ApplicationController < ActionController:x:Base
150
+ # class ApplicationController < ActionController::Base
156
151
  # protect_from_forgery with: CustomStrategy
157
152
  # end
153
+ # * <tt>:store</tt> - Set the strategy to store and retrieve CSRF tokens.
154
+ #
155
+ # Built-in session token strategies are:
156
+ # * <tt>:session</tt> - Store the CSRF token in the session. Used as default if <tt>:store</tt> option is not specified.
157
+ # * <tt>:cookie</tt> - Store the CSRF token in an encrypted cookie.
158
+ #
159
+ # You can also implement custom strategy classes for CSRF token storage:
160
+ #
161
+ # class CustomStore
162
+ # def fetch(request)
163
+ # # Return the token from a custom location
164
+ # end
165
+ #
166
+ # def store(request, csrf_token)
167
+ # # Store the token in a custom location
168
+ # end
169
+ #
170
+ # def reset(request)
171
+ # # Delete the stored session token
172
+ # end
173
+ # end
174
+ #
175
+ # class ApplicationController < ActionController::Base
176
+ # protect_from_forgery store: CustomStore.new
177
+ # end
158
178
  def protect_from_forgery(options = {})
159
179
  options = options.reverse_merge(prepend: false)
160
180
 
161
181
  self.forgery_protection_strategy = protection_method_class(options[:with] || :null_session)
162
182
  self.request_forgery_protection_token ||= :authenticity_token
183
+
184
+ self.csrf_token_storage_strategy = storage_strategy(options[:store] || SessionStore.new)
185
+
163
186
  before_action :verify_authenticity_token, options
164
187
  append_after_action :verify_same_origin_request
165
188
  end
@@ -188,6 +211,22 @@ module ActionController # :nodoc:
188
211
  raise ArgumentError, "Invalid request forgery protection method, use :null_session, :exception, :reset_session, or a custom forgery protection class."
189
212
  end
190
213
  end
214
+
215
+ def storage_strategy(name)
216
+ case name
217
+ when :session
218
+ SessionStore.new
219
+ when :cookie
220
+ CookieStore.new(:csrf_token)
221
+ else
222
+ return name if is_storage_strategy?(name)
223
+ raise ArgumentError, "Invalid CSRF token storage strategy, use :session, :cookie, or a custom CSRF token storage class."
224
+ end
225
+ end
226
+
227
+ def is_storage_strategy?(object)
228
+ object.respond_to?(:fetch) && object.respond_to?(:store) && object.respond_to?(:reset)
229
+ end
191
230
  end
192
231
 
193
232
  module ProtectionMethods
@@ -255,6 +294,68 @@ module ActionController # :nodoc:
255
294
  end
256
295
  end
257
296
 
297
+ class SessionStore
298
+ def fetch(request)
299
+ request.session[:_csrf_token]
300
+ end
301
+
302
+ def store(request, csrf_token)
303
+ request.session[:_csrf_token] = csrf_token
304
+ end
305
+
306
+ def reset(request)
307
+ request.session.delete(:_csrf_token)
308
+ end
309
+ end
310
+
311
+ class CookieStore
312
+ def initialize(cookie = :csrf_token)
313
+ @cookie_name = cookie
314
+ end
315
+
316
+ def fetch(request)
317
+ contents = request.cookie_jar.encrypted[@cookie_name]
318
+ return nil if contents.nil?
319
+
320
+ value = JSON.parse(contents)
321
+ return nil unless value.dig("session_id", "public_id") == request.session.id_was&.public_id
322
+
323
+ value["token"]
324
+ rescue JSON::ParserError
325
+ nil
326
+ end
327
+
328
+ def store(request, csrf_token)
329
+ request.cookie_jar.encrypted.permanent[@cookie_name] = {
330
+ value: {
331
+ token: csrf_token,
332
+ session_id: request.session.id,
333
+ }.to_json,
334
+ httponly: true,
335
+ same_site: :lax,
336
+ }
337
+ end
338
+
339
+ def reset(request)
340
+ request.cookie_jar.delete(@cookie_name)
341
+ end
342
+ end
343
+
344
+ def initialize(...)
345
+ super
346
+ @marked_for_same_origin_verification = nil
347
+ end
348
+
349
+ def reset_csrf_token(request) # :doc:
350
+ request.env.delete(CSRF_TOKEN)
351
+ csrf_token_storage_strategy.reset(request)
352
+ end
353
+
354
+ def commit_csrf_token(request) # :doc:
355
+ csrf_token = request.env[CSRF_TOKEN]
356
+ csrf_token_storage_strategy.store(request, csrf_token) unless csrf_token.nil?
357
+ end
358
+
258
359
  private
259
360
  # The actual before_action that is used to verify the CSRF token.
260
361
  # Don't override this directly. Provide your own forgery protection
@@ -356,20 +457,20 @@ module ActionController # :nodoc:
356
457
 
357
458
  # Creates the authenticity token for the current request.
358
459
  def form_authenticity_token(form_options: {}) # :doc:
359
- masked_authenticity_token(session, form_options: form_options)
460
+ masked_authenticity_token(form_options: form_options)
360
461
  end
361
462
 
362
463
  # Creates a masked version of the authenticity token that varies
363
464
  # on each request. The masking is used to mitigate SSL attacks
364
465
  # like BREACH.
365
- def masked_authenticity_token(session, form_options: {})
466
+ def masked_authenticity_token(form_options: {})
366
467
  action, method = form_options.values_at(:action, :method)
367
468
 
368
469
  raw_token = if per_form_csrf_tokens && action && method
369
470
  action_path = normalize_action_path(action)
370
- per_form_csrf_token(session, action_path, method)
471
+ per_form_csrf_token(nil, action_path, method)
371
472
  else
372
- global_csrf_token(session)
473
+ global_csrf_token
373
474
  end
374
475
 
375
476
  mask_token(raw_token)
@@ -397,14 +498,14 @@ module ActionController # :nodoc:
397
498
  # This is actually an unmasked token. This is expected if
398
499
  # you have just upgraded to masked tokens, but should stop
399
500
  # happening shortly after installing this gem.
400
- compare_with_real_token masked_token, session
501
+ compare_with_real_token masked_token
401
502
 
402
503
  elsif masked_token.length == AUTHENTICITY_TOKEN_LENGTH * 2
403
504
  csrf_token = unmask_token(masked_token)
404
505
 
405
- compare_with_global_token(csrf_token, session) ||
406
- compare_with_real_token(csrf_token, session) ||
407
- valid_per_form_csrf_token?(csrf_token, session)
506
+ compare_with_global_token(csrf_token) ||
507
+ compare_with_real_token(csrf_token) ||
508
+ valid_per_form_csrf_token?(csrf_token)
408
509
  else
409
510
  false # Token is malformed.
410
511
  end
@@ -425,15 +526,15 @@ module ActionController # :nodoc:
425
526
  encode_csrf_token(masked_token)
426
527
  end
427
528
 
428
- def compare_with_real_token(token, session) # :doc:
529
+ def compare_with_real_token(token, session = nil) # :doc:
429
530
  ActiveSupport::SecurityUtils.fixed_length_secure_compare(token, real_csrf_token(session))
430
531
  end
431
532
 
432
- def compare_with_global_token(token, session) # :doc:
533
+ def compare_with_global_token(token, session = nil) # :doc:
433
534
  ActiveSupport::SecurityUtils.fixed_length_secure_compare(token, global_csrf_token(session))
434
535
  end
435
536
 
436
- def valid_per_form_csrf_token?(token, session) # :doc:
537
+ def valid_per_form_csrf_token?(token, session = nil) # :doc:
437
538
  if per_form_csrf_tokens
438
539
  correct_token = per_form_csrf_token(
439
540
  session,
@@ -447,9 +548,12 @@ module ActionController # :nodoc:
447
548
  end
448
549
  end
449
550
 
450
- def real_csrf_token(session) # :doc:
451
- session[:_csrf_token] ||= generate_csrf_token
452
- decode_csrf_token(session[:_csrf_token])
551
+ def real_csrf_token(_session = nil) # :doc:
552
+ csrf_token = request.env.fetch(CSRF_TOKEN) do
553
+ request.env[CSRF_TOKEN] = csrf_token_storage_strategy.fetch(request) || generate_csrf_token
554
+ end
555
+
556
+ decode_csrf_token(csrf_token)
453
557
  end
454
558
 
455
559
  def per_form_csrf_token(session, action_path, method) # :doc:
@@ -459,7 +563,7 @@ module ActionController # :nodoc:
459
563
  GLOBAL_CSRF_TOKEN_IDENTIFIER = "!real_csrf_token"
460
564
  private_constant :GLOBAL_CSRF_TOKEN_IDENTIFIER
461
565
 
462
- def global_csrf_token(session) # :doc:
566
+ def global_csrf_token(session = nil) # :doc:
463
567
  csrf_token_hmac(session, GLOBAL_CSRF_TOKEN_IDENTIFIER)
464
568
  end
465
569
 
@@ -519,31 +623,15 @@ module ActionController # :nodoc:
519
623
  end
520
624
 
521
625
  def generate_csrf_token # :nodoc:
522
- if urlsafe_csrf_tokens
523
- SecureRandom.urlsafe_base64(AUTHENTICITY_TOKEN_LENGTH)
524
- else
525
- SecureRandom.base64(AUTHENTICITY_TOKEN_LENGTH)
526
- end
626
+ SecureRandom.urlsafe_base64(AUTHENTICITY_TOKEN_LENGTH)
527
627
  end
528
628
 
529
629
  def encode_csrf_token(csrf_token) # :nodoc:
530
- if urlsafe_csrf_tokens
531
- Base64.urlsafe_encode64(csrf_token, padding: false)
532
- else
533
- Base64.strict_encode64(csrf_token)
534
- end
630
+ Base64.urlsafe_encode64(csrf_token, padding: false)
535
631
  end
536
632
 
537
633
  def decode_csrf_token(encoded_csrf_token) # :nodoc:
538
- if urlsafe_csrf_tokens
539
- Base64.urlsafe_decode64(encoded_csrf_token)
540
- else
541
- begin
542
- Base64.strict_decode64(encoded_csrf_token)
543
- rescue ArgumentError
544
- Base64.urlsafe_decode64(encoded_csrf_token)
545
- end
546
- end
634
+ Base64.urlsafe_decode64(encoded_csrf_token)
547
635
  end
548
636
  end
549
637
  end