actionpack 6.1.7.6 → 7.0.0.alpha1

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 (107) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +99 -584
  3. data/MIT-LICENSE +2 -1
  4. data/README.rdoc +2 -3
  5. data/lib/abstract_controller/asset_paths.rb +1 -1
  6. data/lib/abstract_controller/base.rb +7 -21
  7. data/lib/abstract_controller/caching/fragments.rb +2 -2
  8. data/lib/abstract_controller/caching.rb +1 -1
  9. data/lib/abstract_controller/callbacks.rb +9 -8
  10. data/lib/abstract_controller/collector.rb +2 -2
  11. data/lib/abstract_controller/error.rb +1 -1
  12. data/lib/abstract_controller/helpers.rb +3 -2
  13. data/lib/abstract_controller/logger.rb +1 -1
  14. data/lib/abstract_controller/railties/routes_helpers.rb +2 -0
  15. data/lib/abstract_controller/translation.rb +0 -2
  16. data/lib/abstract_controller/url_for.rb +4 -6
  17. data/lib/action_controller/api.rb +1 -1
  18. data/lib/action_controller/log_subscriber.rb +3 -1
  19. data/lib/action_controller/metal/conditional_get.rb +38 -1
  20. data/lib/action_controller/metal/content_security_policy.rb +1 -1
  21. data/lib/action_controller/metal/cookies.rb +1 -1
  22. data/lib/action_controller/metal/data_streaming.rb +5 -13
  23. data/lib/action_controller/metal/exceptions.rb +19 -30
  24. data/lib/action_controller/metal/flash.rb +6 -2
  25. data/lib/action_controller/metal/http_authentication.rb +15 -16
  26. data/lib/action_controller/metal/instrumentation.rb +55 -52
  27. data/lib/action_controller/metal/live.rb +42 -2
  28. data/lib/action_controller/metal/mime_responds.rb +3 -3
  29. data/lib/action_controller/metal/params_wrapper.rb +7 -7
  30. data/lib/action_controller/metal/permissions_policy.rb +1 -1
  31. data/lib/action_controller/metal/query_tags.rb +16 -0
  32. data/lib/action_controller/metal/redirecting.rb +49 -34
  33. data/lib/action_controller/metal/rendering.rb +9 -9
  34. data/lib/action_controller/metal/request_forgery_protection.rb +64 -20
  35. data/lib/action_controller/metal/rescue.rb +1 -1
  36. data/lib/action_controller/metal/streaming.rb +1 -3
  37. data/lib/action_controller/metal/strong_parameters.rb +25 -29
  38. data/lib/action_controller/metal/testing.rb +0 -2
  39. data/lib/action_controller/metal.rb +7 -10
  40. data/lib/action_controller/railtie.rb +42 -5
  41. data/lib/action_controller/test_case.rb +6 -2
  42. data/lib/action_controller.rb +2 -5
  43. data/lib/action_dispatch/http/cache.rb +14 -7
  44. data/lib/action_dispatch/http/content_security_policy.rb +47 -37
  45. data/lib/action_dispatch/http/filter_parameters.rb +5 -0
  46. data/lib/action_dispatch/http/mime_negotiation.rb +13 -3
  47. data/lib/action_dispatch/http/mime_type.rb +9 -11
  48. data/lib/action_dispatch/http/parameters.rb +4 -4
  49. data/lib/action_dispatch/http/permissions_policy.rb +1 -1
  50. data/lib/action_dispatch/http/request.rb +10 -19
  51. data/lib/action_dispatch/http/response.rb +3 -3
  52. data/lib/action_dispatch/http/url.rb +9 -10
  53. data/lib/action_dispatch/journey/gtg/builder.rb +11 -12
  54. data/lib/action_dispatch/journey/gtg/simulator.rb +10 -4
  55. data/lib/action_dispatch/journey/gtg/transition_table.rb +77 -21
  56. data/lib/action_dispatch/journey/nodes/node.rb +70 -5
  57. data/lib/action_dispatch/journey/path/pattern.rb +22 -13
  58. data/lib/action_dispatch/journey/route.rb +5 -12
  59. data/lib/action_dispatch/journey/router/utils.rb +2 -2
  60. data/lib/action_dispatch/journey/router.rb +1 -1
  61. data/lib/action_dispatch/journey/routes.rb +3 -3
  62. data/lib/action_dispatch/journey/visualizer/fsm.js +49 -24
  63. data/lib/action_dispatch/journey/visualizer/index.html.erb +1 -1
  64. data/lib/action_dispatch/middleware/actionable_exceptions.rb +0 -1
  65. data/lib/action_dispatch/middleware/cookies.rb +27 -31
  66. data/lib/action_dispatch/middleware/debug_exceptions.rb +6 -4
  67. data/lib/action_dispatch/middleware/debug_locks.rb +3 -3
  68. data/lib/action_dispatch/middleware/exception_wrapper.rb +4 -0
  69. data/lib/action_dispatch/middleware/executor.rb +1 -1
  70. data/lib/action_dispatch/middleware/flash.rb +9 -11
  71. data/lib/action_dispatch/middleware/host_authorization.rb +25 -73
  72. data/lib/action_dispatch/middleware/remote_ip.rb +16 -4
  73. data/lib/action_dispatch/middleware/session/abstract_store.rb +1 -1
  74. data/lib/action_dispatch/middleware/show_exceptions.rb +6 -18
  75. data/lib/action_dispatch/middleware/stack.rb +50 -9
  76. data/lib/action_dispatch/middleware/static.rb +2 -5
  77. data/lib/action_dispatch/middleware/templates/rescues/_message_and_suggestions.html.erb +1 -1
  78. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb +4 -11
  79. data/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +2 -2
  80. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb +2 -2
  81. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +4 -4
  82. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +3 -3
  83. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +28 -18
  84. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb +3 -3
  85. data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +3 -3
  86. data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +3 -3
  87. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +3 -3
  88. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +3 -3
  89. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +5 -14
  90. data/lib/action_dispatch/railtie.rb +8 -2
  91. data/lib/action_dispatch/request/session.rb +43 -13
  92. data/lib/action_dispatch/routing/mapper.rb +44 -72
  93. data/lib/action_dispatch/routing/redirection.rb +0 -2
  94. data/lib/action_dispatch/routing/route_set.rb +7 -4
  95. data/lib/action_dispatch/routing/routes_proxy.rb +1 -1
  96. data/lib/action_dispatch/routing/url_for.rb +1 -2
  97. data/lib/action_dispatch/routing.rb +2 -2
  98. data/lib/action_dispatch/system_test_case.rb +6 -12
  99. data/lib/action_dispatch/system_testing/driver.rb +24 -4
  100. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +10 -6
  101. data/lib/action_dispatch/testing/assertions.rb +2 -5
  102. data/lib/action_dispatch/testing/integration.rb +6 -8
  103. data/lib/action_dispatch/testing/test_process.rb +2 -2
  104. data/lib/action_dispatch.rb +1 -1
  105. data/lib/action_pack/gem_version.rb +4 -4
  106. data/lib/action_pack.rb +1 -1
  107. metadata +21 -21
@@ -16,30 +16,6 @@ module ActionController
16
16
 
17
17
  attr_internal :view_runtime
18
18
 
19
- def process_action(*)
20
- raw_payload = {
21
- controller: self.class.name,
22
- action: action_name,
23
- request: request,
24
- params: request.filtered_parameters,
25
- headers: request.headers,
26
- format: request.format.ref,
27
- method: request.request_method,
28
- path: request.fullpath
29
- }
30
-
31
- ActiveSupport::Notifications.instrument("start_processing.action_controller", raw_payload)
32
-
33
- ActiveSupport::Notifications.instrument("process_action.action_controller", raw_payload) do |payload|
34
- result = super
35
- payload[:response] = response
36
- payload[:status] = response.status
37
- result
38
- ensure
39
- append_info_to_payload(payload)
40
- end
41
- end
42
-
43
19
  def render(*)
44
20
  render_output = nil
45
21
  self.view_runtime = cleanup_view_runtime do
@@ -70,37 +46,64 @@ module ActionController
70
46
  end
71
47
  end
72
48
 
73
- private
74
- # A hook invoked every time a before callback is halted.
75
- def halted_callback_hook(filter, _)
76
- ActiveSupport::Notifications.instrument("halted_callback.action_controller", filter: filter)
77
- end
49
+ private
50
+ def process_action(*)
51
+ raw_payload = {
52
+ controller: self.class.name,
53
+ action: action_name,
54
+ request: request,
55
+ params: request.filtered_parameters,
56
+ headers: request.headers,
57
+ format: request.format.ref,
58
+ method: request.request_method,
59
+ path: request.fullpath
60
+ }
78
61
 
79
- # A hook which allows you to clean up any time, wrongly taken into account in
80
- # views, like database querying time.
81
- #
82
- # def cleanup_view_runtime
83
- # super - time_taken_in_something_expensive
84
- # end
85
- def cleanup_view_runtime # :doc:
86
- yield
87
- end
62
+ ActiveSupport::Notifications.instrument("start_processing.action_controller", raw_payload)
88
63
 
89
- # Every time after an action is processed, this method is invoked
90
- # with the payload, so you can add more information.
91
- def append_info_to_payload(payload) # :doc:
92
- payload[:view_runtime] = view_runtime
93
- end
64
+ ActiveSupport::Notifications.instrument("process_action.action_controller", raw_payload) do |payload|
65
+ result = super
66
+ payload[:response] = response
67
+ payload[:status] = response.status
68
+ result
69
+ rescue => error
70
+ payload[:status] = ActionDispatch::ExceptionWrapper.status_code_for_exception(error.class.name)
71
+ raise
72
+ ensure
73
+ append_info_to_payload(payload)
74
+ end
75
+ end
94
76
 
95
- module ClassMethods
96
- # A hook which allows other frameworks to log what happened during
97
- # controller process action. This method should return an array
98
- # with the messages to be added.
99
- def log_process_action(payload) #:nodoc:
100
- messages, view_runtime = [], payload[:view_runtime]
101
- messages << ("Views: %.1fms" % view_runtime.to_f) if view_runtime
102
- messages
77
+ # A hook invoked every time a before callback is halted.
78
+ def halted_callback_hook(filter, _)
79
+ ActiveSupport::Notifications.instrument("halted_callback.action_controller", filter: filter)
80
+ end
81
+
82
+ # A hook which allows you to clean up any time, wrongly taken into account in
83
+ # views, like database querying time.
84
+ #
85
+ # def cleanup_view_runtime
86
+ # super - time_taken_in_something_expensive
87
+ # end
88
+ def cleanup_view_runtime # :doc:
89
+ yield
90
+ end
91
+
92
+ # Every time after an action is processed, this method is invoked
93
+ # with the payload, so you can add more information.
94
+ def append_info_to_payload(payload) # :doc:
95
+ payload[:view_runtime] = view_runtime
96
+ end
97
+
98
+ module ClassMethods
99
+ # A hook which allows other frameworks to log what happened during
100
+ # controller process action. This method should return an array
101
+ # with the messages to be added.
102
+ def log_process_action(payload) # :nodoc:
103
+ messages, view_runtime = [], payload[:view_runtime]
104
+ messages << ("Views: %.1fms" % view_runtime.to_f) if view_runtime
105
+ messages
106
+ end
103
107
  end
104
- end
105
108
  end
106
109
  end
@@ -124,7 +124,7 @@ module ActionController
124
124
  class ClientDisconnected < RuntimeError
125
125
  end
126
126
 
127
- class Buffer < ActionDispatch::Response::Buffer #:nodoc:
127
+ class Buffer < ActionDispatch::Response::Buffer # :nodoc:
128
128
  include MonitorMixin
129
129
 
130
130
  class << self
@@ -168,6 +168,11 @@ module ActionController
168
168
  end
169
169
  end
170
170
 
171
+ # Same as +write+ but automatically include a newline at the end of the string.
172
+ def writeln(string)
173
+ write string.end_with?("\n") ? string : "#{string}\n"
174
+ end
175
+
171
176
  # Write a 'close' event to the buffer; the producer/writing thread
172
177
  # uses this to notify us that it's finished supplying content.
173
178
  #
@@ -225,7 +230,7 @@ module ActionController
225
230
  end
226
231
  end
227
232
 
228
- class Response < ActionDispatch::Response #:nodoc: all
233
+ class Response < ActionDispatch::Response # :nodoc: all
229
234
  private
230
235
  def before_committed
231
236
  super
@@ -291,6 +296,41 @@ module ActionController
291
296
  response.close if response
292
297
  end
293
298
 
299
+ # Sends a stream to the browser, which is helpful when you're generating exports or other running data where you
300
+ # don't want the entire file buffered in memory first. Similar to send_data, but where the data is generated live.
301
+ #
302
+ # Options:
303
+ # * <tt>:filename</tt> - suggests a filename for the browser to use.
304
+ # * <tt>:type</tt> - specifies an HTTP content type.
305
+ # You can specify either a string or a symbol for a registered type with <tt>Mime::Type.register</tt>, for example :json.
306
+ # If omitted, type will be inferred from the file extension specified in <tt>:filename</tt>.
307
+ # If no content type is registered for the extension, the default type 'application/octet-stream' will be used.
308
+ # * <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded.
309
+ # Valid values are 'inline' and 'attachment' (default).
310
+ #
311
+ # Example of generating a csv export:
312
+ #
313
+ # send_stream(filename: "subscribers.csv") do |stream|
314
+ # stream.write "email_address,updated_at\n"
315
+ #
316
+ # @subscribers.find_each do |subscriber|
317
+ # stream.write "#{subscriber.email_address},#{subscriber.updated_at}\n"
318
+ # end
319
+ # end
320
+ def send_stream(filename:, disposition: "attachment", type: nil)
321
+ response.headers["Content-Type"] =
322
+ (type.is_a?(Symbol) ? Mime[type].to_s : type) ||
323
+ Mime::Type.lookup_by_extension(File.extname(filename).downcase.delete(".")) ||
324
+ "application/octet-stream"
325
+
326
+ response.headers["Content-Disposition"] =
327
+ ActionDispatch::Http::ContentDisposition.format(disposition: disposition, filename: filename)
328
+
329
+ yield response.stream
330
+ ensure
331
+ response.stream.close
332
+ end
333
+
294
334
  private
295
335
  # Spawn a new thread to serve up the controller in. This is to get
296
336
  # around the fact that Rack isn't based around IOs and we need to use
@@ -2,7 +2,7 @@
2
2
 
3
3
  require "abstract_controller/collector"
4
4
 
5
- module ActionController #:nodoc:
5
+ module ActionController # :nodoc:
6
6
  module MimeResponds
7
7
  # Without web-service support, an action which collects the data for displaying a list of people
8
8
  # might look something like this:
@@ -103,7 +103,7 @@ module ActionController #:nodoc:
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.
105
105
  #
106
- # Mime::Type.register "image/jpg", :jpg
106
+ # Mime::Type.register "image/jpeg", :jpg
107
107
  #
108
108
  # +respond_to+ also allows you to specify a common block for different formats by using +any+:
109
109
  #
@@ -289,7 +289,7 @@ module ActionController #:nodoc:
289
289
  @format = request.negotiate_mime(@responses.keys)
290
290
  end
291
291
 
292
- class VariantCollector #:nodoc:
292
+ class VariantCollector # :nodoc:
293
293
  def initialize(variant = nil)
294
294
  @variant = variant
295
295
  @variants = {}
@@ -242,14 +242,14 @@ module ActionController
242
242
  end
243
243
  end
244
244
 
245
- # Performs parameters wrapping upon the request. Called automatically
246
- # by the metal call stack.
247
- def process_action(*)
248
- _perform_parameter_wrapping if _wrapper_enabled?
249
- super
250
- end
251
-
252
245
  private
246
+ # Performs parameters wrapping upon the request. Called automatically
247
+ # by the metal call stack.
248
+ def process_action(*)
249
+ _perform_parameter_wrapping if _wrapper_enabled?
250
+ super
251
+ end
252
+
253
253
  # Returns the wrapper key which will be used to store wrapped parameters.
254
254
  def _wrapper_key
255
255
  _wrapper_options.name
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module ActionController #:nodoc:
3
+ module ActionController # :nodoc:
4
4
  # HTTP Permissions Policy is a web standard for defining a mechanism to
5
5
  # allow and deny the use of browser permissions in its own context, and
6
6
  # in content within any <iframe> elements in the document.
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionController
4
+ module QueryTags # :nodoc:
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ around_action :expose_controller_to_query_logs
9
+ end
10
+
11
+ private
12
+ def expose_controller_to_query_logs(&block)
13
+ ActiveRecord::QueryLogs.set_context(controller: self, &block)
14
+ end
15
+ end
16
+ end
@@ -7,9 +7,9 @@ module ActionController
7
7
  include AbstractController::Logger
8
8
  include ActionController::UrlFor
9
9
 
10
- ILLEGAL_HEADER_VALUE_REGEX = /[\x00-\x08\x0A-\x1F]/.freeze
11
-
12
- class UnsafeRedirectError < StandardError; end
10
+ included do
11
+ mattr_accessor :raise_on_open_redirects, default: false
12
+ end
13
13
 
14
14
  # Redirects the browser to the target specified in +options+. This parameter can be any one of:
15
15
  #
@@ -58,20 +58,28 @@ module ActionController
58
58
  #
59
59
  # Statements after +redirect_to+ in our controller get executed, so +redirect_to+ doesn't stop the execution of the function.
60
60
  # To terminate the execution of the function immediately after the +redirect_to+, use return.
61
+ #
61
62
  # redirect_to post_url(@post) and return
63
+ #
64
+ # Passing user input directly into +redirect_to+ is considered dangerous (e.g. `redirect_to(params[:location])`).
65
+ # Always use regular expressions or a permitted list when redirecting to a user specified location.
62
66
  def redirect_to(options = {}, response_options = {})
67
+ response_options[:allow_other_host] ||= _allow_other_host unless response_options.key?(:allow_other_host)
68
+
63
69
  raise ActionControllerError.new("Cannot redirect to nil!") unless options
64
70
  raise AbstractController::DoubleRenderError if response_body
65
71
 
66
72
  self.status = _extract_redirect_to_status(options, response_options)
67
-
68
- redirect_to_location = _compute_redirect_to_location(request, options)
69
- _ensure_url_is_http_header_safe(redirect_to_location)
70
-
71
- self.location = redirect_to_location
73
+ self.location = _compute_safe_redirect_to_location(request, options, response_options)
72
74
  self.response_body = "<html><body>You are being <a href=\"#{ERB::Util.unwrapped_html_escape(response.location)}\">redirected</a>.</body></html>"
73
75
  end
74
76
 
77
+ # Soft deprecated alias for <tt>redirect_back_or_to</tt> where the fallback_location location is supplied as a keyword argument instead
78
+ # of the first positional argument.
79
+ def redirect_back(fallback_location:, allow_other_host: _allow_other_host, **args)
80
+ redirect_back_or_to fallback_location, allow_other_host: allow_other_host, **args
81
+ end
82
+
75
83
  # Redirects the browser to the page that issued the request (the referrer)
76
84
  # if possible, otherwise redirects to the provided default fallback
77
85
  # location.
@@ -81,35 +89,49 @@ module ActionController
81
89
  # subject to browser security settings and user preferences. If the request
82
90
  # is missing this header, the <tt>fallback_location</tt> will be used.
83
91
  #
84
- # redirect_back fallback_location: { action: "show", id: 5 }
85
- # redirect_back fallback_location: @post
86
- # redirect_back fallback_location: "http://www.rubyonrails.org"
87
- # redirect_back fallback_location: "/images/screenshot.jpg"
88
- # redirect_back fallback_location: posts_url
89
- # redirect_back fallback_location: proc { edit_post_url(@post) }
90
- # redirect_back fallback_location: '/', allow_other_host: false
92
+ # redirect_back_or_to({ action: "show", id: 5 })
93
+ # redirect_back_or_to @post
94
+ # redirect_back_or_to "http://www.rubyonrails.org"
95
+ # redirect_back_or_to "/images/screenshot.jpg"
96
+ # redirect_back_or_to posts_url
97
+ # redirect_back_or_to proc { edit_post_url(@post) }
98
+ # redirect_back_or_to '/', allow_other_host: false
91
99
  #
92
100
  # ==== Options
93
- # * <tt>:fallback_location</tt> - The default fallback location that will be used on missing +Referer+ header.
94
101
  # * <tt>:allow_other_host</tt> - Allow or disallow redirection to the host that is different to the current host, defaults to true.
95
102
  #
96
103
  # All other options that can be passed to #redirect_to are accepted as
97
104
  # options and the behavior is identical.
98
- def redirect_back(fallback_location:, allow_other_host: true, **args)
99
- referer = request.headers["Referer"]
100
- redirect_to_referer = referer && (allow_other_host || _url_host_allowed?(referer))
101
- redirect_to redirect_to_referer ? referer : fallback_location, **args
105
+ def redirect_back_or_to(fallback_location, allow_other_host: _allow_other_host, **options)
106
+ location = request.referer || fallback_location
107
+ location = fallback_location unless allow_other_host || _url_host_allowed?(request.referer)
108
+ allow_other_host = true if _allow_other_host && !allow_other_host # if the fallback is an open redirect
109
+
110
+ redirect_to location, allow_other_host: allow_other_host, **options
102
111
  end
103
112
 
104
- def _compute_redirect_to_location(request, options) #:nodoc:
113
+ def _compute_safe_redirect_to_location(request, options, response_options)
114
+ location = _compute_redirect_to_location(request, options)
115
+
116
+ if response_options[:allow_other_host] || _url_host_allowed?(location)
117
+ location
118
+ else
119
+ raise(ArgumentError, <<~MSG.squish)
120
+ Unsafe redirect #{location.truncate(100).inspect},
121
+ use :allow_other_host to redirect anyway.
122
+ MSG
123
+ end
124
+ end
125
+
126
+ def _compute_redirect_to_location(request, options) # :nodoc:
105
127
  case options
106
128
  # The scheme name consist of a letter followed by any combination of
107
129
  # letters, digits, and the plus ("+"), period ("."), or hyphen ("-")
108
130
  # characters; and is terminated by a colon (":").
109
131
  # See https://tools.ietf.org/html/rfc3986#section-3.1
110
132
  # The protocol relative scheme starts with a double slash "//".
111
- when /\A([a-z][a-z\d\-+\.]*:|\/\/).*/i
112
- options
133
+ when /\A([a-z][a-z\d\-+.]*:|\/\/).*/i
134
+ options.to_str
113
135
  when String
114
136
  request.protocol + request.host_with_port + options
115
137
  when Proc
@@ -122,6 +144,10 @@ module ActionController
122
144
  public :_compute_redirect_to_location
123
145
 
124
146
  private
147
+ def _allow_other_host
148
+ !raise_on_open_redirects
149
+ end
150
+
125
151
  def _extract_redirect_to_status(options, response_options)
126
152
  if options.is_a?(Hash) && options.key?(:status)
127
153
  Rack::Utils.status_code(options.delete(:status))
@@ -137,16 +163,5 @@ module ActionController
137
163
  rescue ArgumentError, URI::Error
138
164
  false
139
165
  end
140
-
141
- def _ensure_url_is_http_header_safe(url)
142
- # Attempt to comply with the set of valid token characters
143
- # defined for an HTTP header value in
144
- # https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.6
145
- if url.match(ILLEGAL_HEADER_VALUE_REGEX)
146
- msg = "The redirect URL #{url} contains one or more illegal HTTP header field character. " \
147
- "Set of legal characters defined in https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.6"
148
- raise UnsafeRedirectError, msg
149
- end
150
- end
151
166
  end
152
167
  end
@@ -24,14 +24,8 @@ module ActionController
24
24
  end
25
25
  end
26
26
 
27
- # Before processing, set the request formats in current controller formats.
28
- def process_action(*) #:nodoc:
29
- self.formats = request.formats.map(&:ref).compact
30
- super
31
- end
32
-
33
27
  # Check for double render errors and set the content_type after rendering.
34
- def render(*args) #:nodoc:
28
+ def render(*args) # :nodoc:
35
29
  raise ::AbstractController::DoubleRenderError if response_body
36
30
  super
37
31
  end
@@ -53,6 +47,12 @@ module ActionController
53
47
  end
54
48
 
55
49
  private
50
+ # Before processing, set the request formats in current controller formats.
51
+ def process_action(*) # :nodoc:
52
+ self.formats = request.formats.filter_map(&:ref)
53
+ super
54
+ end
55
+
56
56
  def _process_variant(options)
57
57
  if defined?(request) && !request.nil? && request.variant.present?
58
58
  options[:variant] = request.variant
@@ -78,8 +78,8 @@ module ActionController
78
78
  end
79
79
 
80
80
  def _set_vary_header
81
- if response.headers["Vary"].blank? && request.should_apply_vary_header?
82
- response.headers["Vary"] = "Accept"
81
+ if self.headers["Vary"].blank? && request.should_apply_vary_header?
82
+ self.headers["Vary"] = "Accept"
83
83
  end
84
84
  end
85
85
 
@@ -4,11 +4,11 @@ require "rack/session/abstract/id"
4
4
  require "action_controller/metal/exceptions"
5
5
  require "active_support/security_utils"
6
6
 
7
- module ActionController #:nodoc:
8
- class InvalidAuthenticityToken < ActionControllerError #:nodoc:
7
+ module ActionController # :nodoc:
8
+ class InvalidAuthenticityToken < ActionControllerError # :nodoc:
9
9
  end
10
10
 
11
- class InvalidCrossOriginRequest < ActionControllerError #:nodoc:
11
+ class InvalidCrossOriginRequest < ActionControllerError # :nodoc:
12
12
  end
13
13
 
14
14
  # Controller actions are protected from Cross-Site Request Forgery (CSRF) attacks
@@ -57,6 +57,17 @@ module ActionController #:nodoc:
57
57
  module RequestForgeryProtection
58
58
  extend ActiveSupport::Concern
59
59
 
60
+ class DisabledSessionError < StandardError
61
+ MESSAGE = <<~EOS.squish
62
+ Request forgery protection requires a working session store but your application has sessions disabled.
63
+ You need to either disable request forgery protection, or configure a working session store.
64
+ EOS
65
+
66
+ def initialize(message = MESSAGE)
67
+ super
68
+ end
69
+ end
70
+
60
71
  include AbstractController::Helpers
61
72
  include AbstractController::Callbacks
62
73
 
@@ -90,6 +101,11 @@ module ActionController #:nodoc:
90
101
  config_accessor :default_protect_from_forgery
91
102
  self.default_protect_from_forgery = false
92
103
 
104
+ # Controls whether trying to use forgery protection without a working session store
105
+ # issues a warning or raises an error.
106
+ config_accessor :silence_disabled_session_errors
107
+ self.silence_disabled_session_errors = true
108
+
93
109
  # Controls whether URL-safe CSRF tokens are generated.
94
110
  config_accessor :urlsafe_csrf_tokens, instance_writer: false
95
111
  self.urlsafe_csrf_tokens = false
@@ -170,7 +186,7 @@ module ActionController #:nodoc:
170
186
  end
171
187
 
172
188
  private
173
- class NullSessionHash < Rack::Session::Abstract::SessionHash #:nodoc:
189
+ class NullSessionHash < Rack::Session::Abstract::SessionHash # :nodoc:
174
190
  def initialize(req)
175
191
  super(nil, req)
176
192
  @data = {}
@@ -183,9 +199,13 @@ module ActionController #:nodoc:
183
199
  def exists?
184
200
  true
185
201
  end
202
+
203
+ def enabled?
204
+ false
205
+ end
186
206
  end
187
207
 
188
- class NullCookieJar < ActionDispatch::Cookies::CookieJar #:nodoc:
208
+ class NullCookieJar < ActionDispatch::Cookies::CookieJar # :nodoc:
189
209
  def write(*)
190
210
  # nothing
191
211
  end
@@ -203,12 +223,14 @@ module ActionController #:nodoc:
203
223
  end
204
224
 
205
225
  class Exception
226
+ attr_accessor :warning_message
227
+
206
228
  def initialize(controller)
207
229
  @controller = controller
208
230
  end
209
231
 
210
232
  def handle_unverified_request
211
- raise ActionController::InvalidAuthenticityToken
233
+ raise ActionController::InvalidAuthenticityToken, warning_message
212
234
  end
213
235
  end
214
236
  end
@@ -228,22 +250,31 @@ module ActionController #:nodoc:
228
250
  mark_for_same_origin_verification!
229
251
 
230
252
  if !verified_request?
231
- if logger && log_warning_on_csrf_failure
232
- if valid_request_origin?
233
- logger.warn "Can't verify CSRF token authenticity."
234
- else
235
- logger.warn "HTTP Origin header (#{request.origin}) didn't match request.base_url (#{request.base_url})"
236
- end
237
- end
253
+ logger.warn unverified_request_warning_message if logger && log_warning_on_csrf_failure
254
+
238
255
  handle_unverified_request
239
256
  end
240
257
  end
241
258
 
242
259
  def handle_unverified_request # :doc:
243
- forgery_protection_strategy.new(self).handle_unverified_request
260
+ protection_strategy = forgery_protection_strategy.new(self)
261
+
262
+ if protection_strategy.respond_to?(:warning_message)
263
+ protection_strategy.warning_message = unverified_request_warning_message
264
+ end
265
+
266
+ protection_strategy.handle_unverified_request
267
+ end
268
+
269
+ def unverified_request_warning_message # :nodoc:
270
+ if valid_request_origin?
271
+ "Can't verify CSRF token authenticity."
272
+ else
273
+ "HTTP Origin header (#{request.origin}) didn't match request.base_url (#{request.base_url})"
274
+ end
244
275
  end
245
276
 
246
- #:nodoc:
277
+ # :nodoc:
247
278
  CROSS_ORIGIN_JAVASCRIPT_WARNING = "Security warning: an embedded " \
248
279
  "<script> tag on another site requested protected JavaScript. " \
249
280
  "If you know what you're doing, go ahead and disable forgery " \
@@ -303,15 +334,15 @@ module ActionController #:nodoc:
303
334
  [form_authenticity_param, request.x_csrf_token]
304
335
  end
305
336
 
306
- # Sets the token value for the current session.
307
- def form_authenticity_token(form_options: {})
337
+ # Creates the authenticity token for the current request.
338
+ def form_authenticity_token(form_options: {}) # :doc:
308
339
  masked_authenticity_token(session, form_options: form_options)
309
340
  end
310
341
 
311
342
  # Creates a masked version of the authenticity token that varies
312
343
  # on each request. The masking is used to mitigate SSL attacks
313
344
  # like BREACH.
314
- def masked_authenticity_token(session, form_options: {}) # :doc:
345
+ def masked_authenticity_token(session, form_options: {})
315
346
  action, method = form_options.values_at(:action, :method)
316
347
 
317
348
  raw_token = if per_form_csrf_tokens && action && method
@@ -438,7 +469,20 @@ module ActionController #:nodoc:
438
469
 
439
470
  # Checks if the controller allows forgery protection.
440
471
  def protect_against_forgery? # :doc:
441
- allow_forgery_protection
472
+ allow_forgery_protection && ensure_session_is_enabled!
473
+ end
474
+
475
+ def ensure_session_is_enabled!
476
+ if !session.respond_to?(:enabled?) || session.enabled?
477
+ true
478
+ else
479
+ if silence_disabled_session_errors
480
+ ActiveSupport::Deprecation.warn(DisabledSessionError::MESSAGE)
481
+ false
482
+ else
483
+ raise DisabledSessionError
484
+ end
485
+ end
442
486
  end
443
487
 
444
488
  NULL_ORIGIN_MESSAGE = <<~MSG
@@ -469,7 +513,7 @@ module ActionController #:nodoc:
469
513
 
470
514
  def generate_csrf_token # :nodoc:
471
515
  if urlsafe_csrf_tokens
472
- SecureRandom.urlsafe_base64(AUTHENTICITY_TOKEN_LENGTH, padding: false)
516
+ SecureRandom.urlsafe_base64(AUTHENTICITY_TOKEN_LENGTH)
473
517
  else
474
518
  SecureRandom.base64(AUTHENTICITY_TOKEN_LENGTH)
475
519
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module ActionController #:nodoc:
3
+ module ActionController # :nodoc:
4
4
  # This module is responsible for providing +rescue_from+ helpers
5
5
  # to controllers and configuring when detailed exceptions must be
6
6
  # shown.
@@ -2,7 +2,7 @@
2
2
 
3
3
  require "rack/chunked"
4
4
 
5
- module ActionController #:nodoc:
5
+ module ActionController # :nodoc:
6
6
  # Allows views to be streamed back to the client as they are rendered.
7
7
  #
8
8
  # By default, Rails renders views by first rendering the template
@@ -193,8 +193,6 @@ module ActionController #:nodoc:
193
193
  # To be described.
194
194
  #
195
195
  module Streaming
196
- extend ActiveSupport::Concern
197
-
198
196
  private
199
197
  # Set proper cache control and transfer encoding when streaming
200
198
  def _process_options(options)