actionpack 5.2.4.5 → 6.0.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 (107) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +111 -384
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +1 -1
  5. data/lib/abstract_controller/base.rb +4 -2
  6. data/lib/abstract_controller/caching/fragments.rb +6 -21
  7. data/lib/abstract_controller/callbacks.rb +12 -0
  8. data/lib/abstract_controller/collector.rb +1 -1
  9. data/lib/abstract_controller/helpers.rb +2 -2
  10. data/lib/abstract_controller/railties/routes_helpers.rb +1 -1
  11. data/lib/action_controller.rb +1 -0
  12. data/lib/action_controller/api.rb +2 -1
  13. data/lib/action_controller/base.rb +2 -7
  14. data/lib/action_controller/caching.rb +1 -1
  15. data/lib/action_controller/log_subscriber.rb +8 -5
  16. data/lib/action_controller/metal.rb +2 -2
  17. data/lib/action_controller/metal/conditional_get.rb +9 -3
  18. data/lib/action_controller/metal/data_streaming.rb +5 -6
  19. data/lib/action_controller/metal/default_headers.rb +17 -0
  20. data/lib/action_controller/metal/exceptions.rb +22 -1
  21. data/lib/action_controller/metal/flash.rb +5 -5
  22. data/lib/action_controller/metal/force_ssl.rb +17 -57
  23. data/lib/action_controller/metal/head.rb +1 -1
  24. data/lib/action_controller/metal/helpers.rb +1 -2
  25. data/lib/action_controller/metal/http_authentication.rb +20 -21
  26. data/lib/action_controller/metal/implicit_render.rb +2 -12
  27. data/lib/action_controller/metal/instrumentation.rb +3 -5
  28. data/lib/action_controller/metal/live.rb +28 -26
  29. data/lib/action_controller/metal/mime_responds.rb +13 -2
  30. data/lib/action_controller/metal/params_wrapper.rb +18 -14
  31. data/lib/action_controller/metal/redirecting.rb +32 -11
  32. data/lib/action_controller/metal/rendering.rb +1 -1
  33. data/lib/action_controller/metal/request_forgery_protection.rb +26 -40
  34. data/lib/action_controller/metal/strong_parameters.rb +57 -34
  35. data/lib/action_controller/metal/url_for.rb +1 -1
  36. data/lib/action_controller/railties/helpers.rb +1 -1
  37. data/lib/action_controller/renderer.rb +15 -2
  38. data/lib/action_controller/test_case.rb +3 -7
  39. data/lib/action_dispatch.rb +7 -6
  40. data/lib/action_dispatch/http/cache.rb +14 -10
  41. data/lib/action_dispatch/http/content_disposition.rb +45 -0
  42. data/lib/action_dispatch/http/content_security_policy.rb +9 -8
  43. data/lib/action_dispatch/http/filter_parameters.rb +8 -6
  44. data/lib/action_dispatch/http/filter_redirect.rb +1 -1
  45. data/lib/action_dispatch/http/headers.rb +1 -1
  46. data/lib/action_dispatch/http/mime_negotiation.rb +7 -10
  47. data/lib/action_dispatch/http/mime_type.rb +1 -5
  48. data/lib/action_dispatch/http/parameter_filter.rb +5 -79
  49. data/lib/action_dispatch/http/parameters.rb +13 -3
  50. data/lib/action_dispatch/http/request.rb +10 -13
  51. data/lib/action_dispatch/http/response.rb +14 -14
  52. data/lib/action_dispatch/http/upload.rb +5 -0
  53. data/lib/action_dispatch/http/url.rb +81 -81
  54. data/lib/action_dispatch/journey/formatter.rb +1 -1
  55. data/lib/action_dispatch/journey/nfa/simulator.rb +0 -2
  56. data/lib/action_dispatch/journey/nodes/node.rb +9 -8
  57. data/lib/action_dispatch/journey/path/pattern.rb +3 -4
  58. data/lib/action_dispatch/journey/router.rb +0 -3
  59. data/lib/action_dispatch/journey/router/utils.rb +10 -10
  60. data/lib/action_dispatch/journey/scanner.rb +11 -4
  61. data/lib/action_dispatch/journey/visitors.rb +1 -1
  62. data/lib/action_dispatch/middleware/callbacks.rb +2 -4
  63. data/lib/action_dispatch/middleware/cookies.rb +49 -70
  64. data/lib/action_dispatch/middleware/debug_exceptions.rb +32 -58
  65. data/lib/action_dispatch/middleware/debug_locks.rb +5 -5
  66. data/lib/action_dispatch/middleware/debug_view.rb +50 -0
  67. data/lib/action_dispatch/middleware/exception_wrapper.rb +36 -7
  68. data/lib/action_dispatch/middleware/flash.rb +1 -1
  69. data/lib/action_dispatch/middleware/host_authorization.rb +103 -0
  70. data/lib/action_dispatch/middleware/remote_ip.rb +6 -8
  71. data/lib/action_dispatch/middleware/request_id.rb +2 -2
  72. data/lib/action_dispatch/middleware/session/abstract_store.rb +0 -14
  73. data/lib/action_dispatch/middleware/session/cache_store.rb +6 -11
  74. data/lib/action_dispatch/middleware/session/cookie_store.rb +11 -27
  75. data/lib/action_dispatch/middleware/ssl.rb +8 -8
  76. data/lib/action_dispatch/middleware/stack.rb +2 -2
  77. data/lib/action_dispatch/middleware/static.rb +5 -6
  78. data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +4 -2
  79. data/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +45 -35
  80. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb +7 -0
  81. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.text.erb +5 -0
  82. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +20 -2
  83. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +4 -4
  84. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +2 -2
  85. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb +19 -0
  86. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.text.erb +3 -0
  87. data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +2 -2
  88. data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +1 -1
  89. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +2 -2
  90. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +3 -0
  91. data/lib/action_dispatch/railtie.rb +1 -0
  92. data/lib/action_dispatch/request/session.rb +8 -6
  93. data/lib/action_dispatch/routing.rb +3 -2
  94. data/lib/action_dispatch/routing/inspector.rb +99 -50
  95. data/lib/action_dispatch/routing/mapper.rb +36 -29
  96. data/lib/action_dispatch/routing/polymorphic_routes.rb +3 -4
  97. data/lib/action_dispatch/routing/route_set.rb +11 -12
  98. data/lib/action_dispatch/routing/url_for.rb +1 -0
  99. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +3 -3
  100. data/lib/action_dispatch/testing/assertions/response.rb +2 -3
  101. data/lib/action_dispatch/testing/assertions/routing.rb +7 -2
  102. data/lib/action_dispatch/testing/integration.rb +11 -4
  103. data/lib/action_dispatch/testing/test_process.rb +2 -2
  104. data/lib/action_dispatch/testing/test_response.rb +4 -32
  105. data/lib/action_pack.rb +1 -1
  106. data/lib/action_pack/gem_version.rb +4 -4
  107. metadata +22 -20
@@ -38,7 +38,7 @@ module ActionController
38
38
  self.response_body = ""
39
39
 
40
40
  if include_content?(response_code)
41
- self.content_type = content_type || (Mime[formats.first] if formats)
41
+ self.content_type = content_type || (Mime[formats.first] if formats) || Mime[:html]
42
42
  response.charset = false
43
43
  end
44
44
 
@@ -100,8 +100,7 @@ module ActionController
100
100
  # # => ["application", "chart", "rubygems"]
101
101
  def all_helpers_from_path(path)
102
102
  helpers = Array(path).flat_map do |_path|
103
- extract = /^#{Regexp.quote(_path.to_s)}\/?(.*)_helper.rb$/
104
- names = Dir["#{_path}/**/*_helper.rb"].map { |file| file.sub(extract, '\1'.freeze) }
103
+ names = Dir["#{_path}/**/*_helper.rb"].map { |file| file[_path.to_s.size + 1..-"_helper.rb".size - 1] }
105
104
  names.sort!
106
105
  end
107
106
  helpers.uniq!
@@ -56,8 +56,9 @@ module ActionController
56
56
  # In your integration tests, you can do something like this:
57
57
  #
58
58
  # def test_access_granted_from_xml
59
- # @request.env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Basic.encode_credentials(users(:dhh).name, users(:dhh).password)
60
- # get "/notes/1.xml"
59
+ # authorization = ActionController::HttpAuthentication::Basic.encode_credentials(users(:dhh).name, users(:dhh).password)
60
+ #
61
+ # get "/notes/1.xml", headers: { 'HTTP_AUTHORIZATION' => authorization }
61
62
  #
62
63
  # assert_equal 200, status
63
64
  # end
@@ -68,21 +69,20 @@ module ActionController
68
69
  extend ActiveSupport::Concern
69
70
 
70
71
  module ClassMethods
71
- def http_basic_authenticate_with(options = {})
72
- before_action(options.except(:name, :password, :realm)) do
73
- authenticate_or_request_with_http_basic(options[:realm] || "Application") do |name, password|
74
- # This comparison uses & so that it doesn't short circuit and
75
- # uses `secure_compare` so that length information
76
- # isn't leaked.
77
- ActiveSupport::SecurityUtils.secure_compare(name, options[:name]) &
78
- ActiveSupport::SecurityUtils.secure_compare(password, options[:password])
79
- end
80
- end
72
+ def http_basic_authenticate_with(name:, password:, realm: nil, **options)
73
+ before_action(options) { http_basic_authenticate_or_request_with name: name, password: password, realm: realm }
74
+ end
75
+ end
76
+
77
+ def http_basic_authenticate_or_request_with(name:, password:, realm: nil, message: nil)
78
+ authenticate_or_request_with_http_basic(realm, message) do |given_name, given_password|
79
+ ActiveSupport::SecurityUtils.secure_compare(given_name, name) &
80
+ ActiveSupport::SecurityUtils.secure_compare(given_password, password)
81
81
  end
82
82
  end
83
83
 
84
- def authenticate_or_request_with_http_basic(realm = "Application", message = nil, &login_procedure)
85
- authenticate_with_http_basic(&login_procedure) || request_http_basic_authentication(realm, message)
84
+ def authenticate_or_request_with_http_basic(realm = nil, message = nil, &login_procedure)
85
+ authenticate_with_http_basic(&login_procedure) || request_http_basic_authentication(realm || "Application", message)
86
86
  end
87
87
 
88
88
  def authenticate_with_http_basic(&login_procedure)
@@ -126,7 +126,7 @@ module ActionController
126
126
 
127
127
  def authentication_request(controller, realm, message)
128
128
  message ||= "HTTP Basic: Access denied.\n"
129
- controller.headers["WWW-Authenticate"] = %(Basic realm="#{realm.tr('"'.freeze, "".freeze)}")
129
+ controller.headers["WWW-Authenticate"] = %(Basic realm="#{realm.tr('"', "")}")
130
130
  controller.status = 401
131
131
  controller.response_body = message
132
132
  end
@@ -389,10 +389,9 @@ module ActionController
389
389
  # In your integration tests, you can do something like this:
390
390
  #
391
391
  # def test_access_granted_from_xml
392
- # get(
393
- # "/notes/1.xml", nil,
394
- # 'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Token.encode_credentials(users(:dhh).token)
395
- # )
392
+ # authorization = ActionController::HttpAuthentication::Token.encode_credentials(users(:dhh).token)
393
+ #
394
+ # get "/notes/1.xml", headers: { 'HTTP_AUTHORIZATION' => authorization }
396
395
  #
397
396
  # assert_equal 200, status
398
397
  # end
@@ -474,7 +473,7 @@ module ActionController
474
473
 
475
474
  # This removes the <tt>"</tt> characters wrapping the value.
476
475
  def rewrite_param_values(array_params)
477
- array_params.each { |param| (param[1] || "".dup).gsub! %r/^"|"$/, "" }
476
+ array_params.each { |param| (param[1] || +"").gsub! %r/^"|"$/, "" }
478
477
  end
479
478
 
480
479
  # This method takes an authorization body and splits up the key-value
@@ -511,7 +510,7 @@ module ActionController
511
510
  # Returns nothing.
512
511
  def authentication_request(controller, realm, message = nil)
513
512
  message ||= "HTTP Token: Access denied.\n"
514
- controller.headers["WWW-Authenticate"] = %(Token realm="#{realm.tr('"'.freeze, "".freeze)}")
513
+ controller.headers["WWW-Authenticate"] = %(Token realm="#{realm.tr('"', "")}")
515
514
  controller.__send__ :render, plain: message, status: :unauthorized
516
515
  end
517
516
  end
@@ -41,18 +41,8 @@ module ActionController
41
41
 
42
42
  raise ActionController::UnknownFormat, message
43
43
  elsif interactive_browser_request?
44
- message = "#{self.class.name}\##{action_name} is missing a template " \
45
- "for this request format and variant.\n\n" \
46
- "request.formats: #{request.formats.map(&:to_s).inspect}\n" \
47
- "request.variant: #{request.variant.inspect}\n\n" \
48
- "NOTE! For XHR/Ajax or API requests, this action would normally " \
49
- "respond with 204 No Content: an empty white screen. Since you're " \
50
- "loading it in a web browser, we assume that you expected to " \
51
- "actually render a template, not nothing, so we're showing an " \
52
- "error to be extra-clear. If you expect 204 No Content, carry on. " \
53
- "That's what you'll get from an XHR or API request. Give it a shot."
54
-
55
- raise ActionController::UnknownFormat, message
44
+ message = "#{self.class.name}\##{action_name} is missing a template for request formats: #{request.formats.map(&:to_s).join(',')}"
45
+ raise ActionController::MissingExactTemplate, message
56
46
  else
57
47
  logger.info "No template found for #{self.class.name}\##{action_name}, rendering head :no_content" if logger
58
48
  super
@@ -30,13 +30,11 @@ module ActionController
30
30
  ActiveSupport::Notifications.instrument("start_processing.action_controller", raw_payload.dup)
31
31
 
32
32
  ActiveSupport::Notifications.instrument("process_action.action_controller", raw_payload) do |payload|
33
- begin
34
- result = super
33
+ super.tap do
35
34
  payload[:status] = response.status
36
- result
37
- ensure
38
- append_info_to_payload(payload)
39
35
  end
36
+ ensure
37
+ append_info_to_payload(payload)
40
38
  end
41
39
  end
42
40
 
@@ -86,7 +86,7 @@ module ActionController
86
86
  # Note: SSEs are not currently supported by IE. However, they are supported
87
87
  # by Chrome, Firefox, Opera, and Safari.
88
88
  class SSE
89
- WHITELISTED_OPTIONS = %w( retry event id )
89
+ PERMITTED_OPTIONS = %w( retry event id )
90
90
 
91
91
  def initialize(stream, options = {})
92
92
  @stream = stream
@@ -111,13 +111,13 @@ module ActionController
111
111
  def perform_write(json, options)
112
112
  current_options = @options.merge(options).stringify_keys
113
113
 
114
- WHITELISTED_OPTIONS.each do |option_name|
114
+ PERMITTED_OPTIONS.each do |option_name|
115
115
  if (option_value = current_options[option_name])
116
116
  @stream.write "#{option_name}: #{option_value}\n"
117
117
  end
118
118
  end
119
119
 
120
- message = json.gsub("\n".freeze, "\ndata: ".freeze)
120
+ message = json.gsub("\n", "\ndata: ")
121
121
  @stream.write "data: #{message}\n\n"
122
122
  end
123
123
  end
@@ -280,33 +280,35 @@ module ActionController
280
280
  raise error if error
281
281
  end
282
282
 
283
- # Spawn a new thread to serve up the controller in. This is to get
284
- # around the fact that Rack isn't based around IOs and we need to use
285
- # a thread to stream data from the response bodies. Nobody should call
286
- # this method except in Rails internals. Seriously!
287
- def new_controller_thread # :nodoc:
288
- Thread.new {
289
- t2 = Thread.current
290
- t2.abort_on_exception = true
291
- yield
292
- }
283
+ def response_body=(body)
284
+ super
285
+ response.close if response
293
286
  end
294
287
 
295
- def log_error(exception)
296
- logger = ActionController::Base.logger
297
- return unless logger
288
+ private
298
289
 
299
- logger.fatal do
300
- message = "\n#{exception.class} (#{exception.message}):\n".dup
301
- message << exception.annoted_source_code.to_s if exception.respond_to?(:annoted_source_code)
302
- message << " " << exception.backtrace.join("\n ")
303
- "#{message}\n\n"
290
+ # Spawn a new thread to serve up the controller in. This is to get
291
+ # around the fact that Rack isn't based around IOs and we need to use
292
+ # a thread to stream data from the response bodies. Nobody should call
293
+ # this method except in Rails internals. Seriously!
294
+ def new_controller_thread # :nodoc:
295
+ Thread.new {
296
+ t2 = Thread.current
297
+ t2.abort_on_exception = true
298
+ yield
299
+ }
304
300
  end
305
- end
306
301
 
307
- def response_body=(body)
308
- super
309
- response.close if response
310
- end
302
+ def log_error(exception)
303
+ logger = ActionController::Base.logger
304
+ return unless logger
305
+
306
+ logger.fatal do
307
+ message = +"\n#{exception.class} (#{exception.message}):\n"
308
+ message << exception.annoted_source_code.to_s if exception.respond_to?(:annoted_source_code)
309
+ message << " " << exception.backtrace.join("\n ")
310
+ "#{message}\n\n"
311
+ end
312
+ end
311
313
  end
312
314
  end
@@ -11,7 +11,7 @@ module ActionController #:nodoc:
11
11
  # @people = Person.all
12
12
  # end
13
13
  #
14
- # That action implicitly responds to all formats, but formats can also be whitelisted:
14
+ # That action implicitly responds to all formats, but formats can also be explicitly enumerated:
15
15
  #
16
16
  # def index
17
17
  # @people = Person.all
@@ -105,7 +105,7 @@ module ActionController #:nodoc:
105
105
  #
106
106
  # Mime::Type.register "image/jpg", :jpg
107
107
  #
108
- # Respond to also allows you to specify a common block for different formats by using +any+:
108
+ # +respond_to+ also allows you to specify a common block for different formats by using +any+:
109
109
  #
110
110
  # def index
111
111
  # @people = Person.all
@@ -124,6 +124,14 @@ module ActionController #:nodoc:
124
124
  #
125
125
  # render json: @people
126
126
  #
127
+ # +any+ can also be used with no arguments, in which case it will be used for any format requested by
128
+ # the user:
129
+ #
130
+ # respond_to do |format|
131
+ # format.html
132
+ # format.any { redirect_to support_path }
133
+ # end
134
+ #
127
135
  # Formats can have different variants.
128
136
  #
129
137
  # The request variant is a specialization of the request format, like <tt>:tablet</tt>,
@@ -197,6 +205,9 @@ module ActionController #:nodoc:
197
205
  yield collector if block_given?
198
206
 
199
207
  if format = collector.negotiate_format(request)
208
+ if content_type && content_type != format
209
+ raise ActionController::RespondToMismatchError
210
+ end
200
211
  _process_format(format)
201
212
  _set_rendered_content_type format
202
213
  response = collector.response
@@ -93,7 +93,7 @@ module ActionController
93
93
  end
94
94
 
95
95
  def model
96
- super || self.model = _default_wrap_model
96
+ super || synchronize { super || self.model = _default_wrap_model }
97
97
  end
98
98
 
99
99
  def include
@@ -115,7 +115,7 @@ module ActionController
115
115
 
116
116
  if m.respond_to?(:nested_attributes_options) && m.nested_attributes_options.keys.any?
117
117
  self.include += m.nested_attributes_options.keys.map do |key|
118
- key.to_s.dup.concat("_attributes")
118
+ key.to_s.concat("_attributes")
119
119
  end
120
120
  end
121
121
 
@@ -241,18 +241,7 @@ module ActionController
241
241
  # Performs parameters wrapping upon the request. Called automatically
242
242
  # by the metal call stack.
243
243
  def process_action(*args)
244
- if _wrapper_enabled?
245
- wrapped_hash = _wrap_parameters request.request_parameters
246
- wrapped_keys = request.request_parameters.keys
247
- wrapped_filtered_hash = _wrap_parameters request.filtered_parameters.slice(*wrapped_keys)
248
-
249
- # This will make the wrapped hash accessible from controller and view.
250
- request.parameters.merge! wrapped_hash
251
- request.request_parameters.merge! wrapped_hash
252
-
253
- # This will display the wrapped hash in the log file.
254
- request.filtered_parameters.merge! wrapped_filtered_hash
255
- end
244
+ _perform_parameter_wrapping if _wrapper_enabled?
256
245
  super
257
246
  end
258
247
 
@@ -289,5 +278,20 @@ module ActionController
289
278
  ref = request.content_mime_type.ref
290
279
  _wrapper_formats.include?(ref) && _wrapper_key && !request.parameters.key?(_wrapper_key)
291
280
  end
281
+
282
+ def _perform_parameter_wrapping
283
+ wrapped_hash = _wrap_parameters request.request_parameters
284
+ wrapped_keys = request.request_parameters.keys
285
+ wrapped_filtered_hash = _wrap_parameters request.filtered_parameters.slice(*wrapped_keys)
286
+
287
+ # This will make the wrapped hash accessible from controller and view.
288
+ request.parameters.merge! wrapped_hash
289
+ request.request_parameters.merge! wrapped_hash
290
+
291
+ # This will display the wrapped hash in the log file.
292
+ request.filtered_parameters.merge! wrapped_filtered_hash
293
+ rescue ActionDispatch::Http::Parameters::ParseError
294
+ # swallow parse error exception
295
+ end
292
296
  end
293
297
  end
@@ -55,12 +55,12 @@ module ActionController
55
55
  # Statements after +redirect_to+ in our controller get executed, so +redirect_to+ doesn't stop the execution of the function.
56
56
  # To terminate the execution of the function immediately after the +redirect_to+, use return.
57
57
  # redirect_to post_url(@post) and return
58
- def redirect_to(options = {}, response_status = {})
58
+ def redirect_to(options = {}, response_options = {})
59
59
  raise ActionControllerError.new("Cannot redirect to nil!") unless options
60
60
  raise AbstractController::DoubleRenderError if response_body
61
61
 
62
- self.status = _extract_redirect_to_status(options, response_status)
63
- self.location = _compute_redirect_to_location(request, options)
62
+ self.status = _extract_redirect_to_status(options, response_options)
63
+ self.location = _compute_safe_redirect_to_location(request, options, response_options)
64
64
  self.response_body = "<html><body>You are being <a href=\"#{ERB::Util.unwrapped_html_escape(response.location)}\">redirected</a>.</body></html>"
65
65
  end
66
66
 
@@ -88,9 +88,13 @@ module ActionController
88
88
  # All other options that can be passed to <tt>redirect_to</tt> are accepted as
89
89
  # options and the behavior is identical.
90
90
  def redirect_back(fallback_location:, allow_other_host: true, **args)
91
- referer = request.headers["Referer"]
92
- redirect_to_referer = referer && (allow_other_host || _url_host_allowed?(referer))
93
- redirect_to redirect_to_referer ? referer : fallback_location, **args
91
+ referer = request.headers.fetch("Referer", fallback_location)
92
+ response_options = {
93
+ fallback_location: fallback_location,
94
+ allow_other_host: allow_other_host,
95
+ **args,
96
+ }
97
+ redirect_to referer, response_options
94
98
  end
95
99
 
96
100
  def _compute_redirect_to_location(request, options) #:nodoc:
@@ -114,18 +118,35 @@ module ActionController
114
118
  public :_compute_redirect_to_location
115
119
 
116
120
  private
117
- def _extract_redirect_to_status(options, response_status)
121
+ def _compute_safe_redirect_to_location(request, options, response_options)
122
+ location = _compute_redirect_to_location(request, options)
123
+ location_options = options.is_a?(Hash) ? options : {}
124
+ if response_options[:allow_other_host] || _url_host_allowed?(location, location_options)
125
+ location
126
+ else
127
+ fallback_location = response_options.fetch(:fallback_location) do
128
+ raise ArgumentError, <<~MSG.squish
129
+ Unsafe redirect #{location.inspect},
130
+ use :fallback_location to specify a fallback
131
+ or :allow_other_host to redirect anyway.
132
+ MSG
133
+ end
134
+ _compute_redirect_to_location(request, fallback_location)
135
+ end
136
+ end
137
+
138
+ def _extract_redirect_to_status(options, response_options)
118
139
  if options.is_a?(Hash) && options.key?(:status)
119
140
  Rack::Utils.status_code(options.delete(:status))
120
- elsif response_status.key?(:status)
121
- Rack::Utils.status_code(response_status[:status])
141
+ elsif response_options.key?(:status)
142
+ Rack::Utils.status_code(response_options[:status])
122
143
  else
123
144
  302
124
145
  end
125
146
  end
126
147
 
127
- def _url_host_allowed?(url)
128
- URI(url.to_s).host == request.host
148
+ def _url_host_allowed?(url, options = {})
149
+ URI(url.to_s).host.in?([request.host, options[:host]])
129
150
  rescue ArgumentError, URI::Error
130
151
  false
131
152
  end
@@ -40,7 +40,7 @@ module ActionController
40
40
  def render_to_string(*)
41
41
  result = super
42
42
  if result.respond_to?(:each)
43
- string = "".dup
43
+ string = +""
44
44
  result.each { |r| string << r }
45
45
  string
46
46
  else
@@ -3,7 +3,6 @@
3
3
  require "rack/session/abstract/id"
4
4
  require "action_controller/metal/exceptions"
5
5
  require "active_support/security_utils"
6
- require "active_support/core_ext/string/strip"
7
6
 
8
7
  module ActionController #:nodoc:
9
8
  class InvalidAuthenticityToken < ActionControllerError #:nodoc:
@@ -18,7 +17,7 @@ module ActionController #:nodoc:
18
17
  # access. When a request reaches your application, \Rails verifies the received
19
18
  # token with the token in the session. All requests are checked except GET requests
20
19
  # as these should be idempotent. Keep in mind that all session-oriented requests
21
- # should be CSRF protected, including JavaScript and HTML requests.
20
+ # are CSRF protected by default, including JavaScript and HTML requests.
22
21
  #
23
22
  # Since HTML and JavaScript requests are typically made from the browser, we
24
23
  # need to ensure to verify request authenticity for the web browser. We can
@@ -31,16 +30,23 @@ module ActionController #:nodoc:
31
30
  # URL on your site. When your JavaScript response loads on their site, it executes.
32
31
  # With carefully crafted JavaScript on their end, sensitive data in your JavaScript
33
32
  # response may be extracted. To prevent this, only XmlHttpRequest (known as XHR or
34
- # Ajax) requests are allowed to make GET requests for JavaScript responses.
33
+ # Ajax) requests are allowed to make requests for JavaScript responses.
35
34
  #
36
- # It's important to remember that XML or JSON requests are also affected and if
37
- # you're building an API you should change forgery protection method in
35
+ # It's important to remember that XML or JSON requests are also checked by default. If
36
+ # you're building an API or an SPA you could change forgery protection method in
38
37
  # <tt>ApplicationController</tt> (by default: <tt>:exception</tt>):
39
38
  #
40
39
  # class ApplicationController < ActionController::Base
41
40
  # protect_from_forgery unless: -> { request.format.json? }
42
41
  # end
43
42
  #
43
+ # It is generally safe to exclude XHR requests from CSRF protection
44
+ # (like the code snippet above does), because XHR requests can only be made from
45
+ # the same origin. Note however that any cross-origin third party domain
46
+ # allowed via {CORS}[https://en.wikipedia.org/wiki/Cross-origin_resource_sharing]
47
+ # will also be able to create XHR requests. Be sure to check your
48
+ # CORS configuration before disabling forgery protection for XHR.
49
+ #
44
50
  # CSRF protection is turned on with the <tt>protect_from_forgery</tt> method.
45
51
  # By default <tt>protect_from_forgery</tt> protects your session with
46
52
  # <tt>:null_session</tt> method, which provides an empty session
@@ -55,7 +61,7 @@ module ActionController #:nodoc:
55
61
  # <tt>csrf_meta_tags</tt> in the HTML +head+.
56
62
  #
57
63
  # Learn more about CSRF attacks and securing your application in the
58
- # {Ruby on Rails Security Guide}[http://guides.rubyonrails.org/security.html].
64
+ # {Ruby on Rails Security Guide}[https://guides.rubyonrails.org/security.html].
59
65
  module RequestForgeryProtection
60
66
  extend ActiveSupport::Concern
61
67
 
@@ -276,7 +282,7 @@ module ActionController #:nodoc:
276
282
 
277
283
  # Check for cross-origin JavaScript responses.
278
284
  def non_xhr_javascript_response? # :doc:
279
- content_type =~ %r(\Atext/javascript) && !request.xhr?
285
+ content_type =~ %r(\A(?:text|application)/javascript) && !request.xhr?
280
286
  end
281
287
 
282
288
  AUTHENTICITY_TOKEN_LENGTH = 32
@@ -318,15 +324,13 @@ module ActionController #:nodoc:
318
324
  action_path = normalize_action_path(action)
319
325
  per_form_csrf_token(session, action_path, method)
320
326
  else
321
- global_csrf_token(session)
327
+ real_csrf_token(session)
322
328
  end
323
329
 
324
330
  one_time_pad = SecureRandom.random_bytes(AUTHENTICITY_TOKEN_LENGTH)
325
331
  encrypted_csrf_token = xor_byte_strings(one_time_pad, raw_token)
326
332
  masked_token = one_time_pad + encrypted_csrf_token
327
- Base64.urlsafe_encode64(masked_token, padding: false)
328
-
329
- mask_token(raw_token)
333
+ Base64.strict_encode64(masked_token)
330
334
  end
331
335
 
332
336
  # Checks the client's masked token to see if it matches the
@@ -356,8 +360,7 @@ module ActionController #:nodoc:
356
360
  elsif masked_token.length == AUTHENTICITY_TOKEN_LENGTH * 2
357
361
  csrf_token = unmask_token(masked_token)
358
362
 
359
- compare_with_global_token(csrf_token, session) ||
360
- compare_with_real_token(csrf_token, session) ||
363
+ compare_with_real_token(csrf_token, session) ||
361
364
  valid_per_form_csrf_token?(csrf_token, session)
362
365
  else
363
366
  false # Token is malformed.
@@ -372,21 +375,10 @@ module ActionController #:nodoc:
372
375
  xor_byte_strings(one_time_pad, encrypted_csrf_token)
373
376
  end
374
377
 
375
- def mask_token(raw_token) # :doc:
376
- one_time_pad = SecureRandom.random_bytes(AUTHENTICITY_TOKEN_LENGTH)
377
- encrypted_csrf_token = xor_byte_strings(one_time_pad, raw_token)
378
- masked_token = one_time_pad + encrypted_csrf_token
379
- Base64.strict_encode64(masked_token)
380
- end
381
-
382
378
  def compare_with_real_token(token, session) # :doc:
383
379
  ActiveSupport::SecurityUtils.fixed_length_secure_compare(token, real_csrf_token(session))
384
380
  end
385
381
 
386
- def compare_with_global_token(token, session) # :doc:
387
- ActiveSupport::SecurityUtils.fixed_length_secure_compare(token, global_csrf_token(session))
388
- end
389
-
390
382
  def valid_per_form_csrf_token?(token, session) # :doc:
391
383
  if per_form_csrf_tokens
392
384
  correct_token = per_form_csrf_token(
@@ -407,28 +399,22 @@ module ActionController #:nodoc:
407
399
  end
408
400
 
409
401
  def per_form_csrf_token(session, action_path, method) # :doc:
410
- csrf_token_hmac(session, [action_path, method.downcase].join("#"))
411
- end
412
-
413
- GLOBAL_CSRF_TOKEN_IDENTIFIER = "!real_csrf_token"
414
- private_constant :GLOBAL_CSRF_TOKEN_IDENTIFIER
415
-
416
- def global_csrf_token(session) # :doc:
417
- csrf_token_hmac(session, GLOBAL_CSRF_TOKEN_IDENTIFIER)
418
- end
419
-
420
- def csrf_token_hmac(session, identifier) # :doc:
421
402
  OpenSSL::HMAC.digest(
422
403
  OpenSSL::Digest::SHA256.new,
423
404
  real_csrf_token(session),
424
- identifier
405
+ [action_path, method.downcase].join("#")
425
406
  )
426
407
  end
427
408
 
428
409
  def xor_byte_strings(s1, s2) # :doc:
429
- s2_bytes = s2.bytes
430
- s1.each_byte.with_index { |c1, i| s2_bytes[i] ^= c1 }
431
- s2_bytes.pack("C*")
410
+ s2 = s2.dup
411
+ size = s1.bytesize
412
+ i = 0
413
+ while i < size
414
+ s2.setbyte(i, s1.getbyte(i) ^ s2.getbyte(i))
415
+ i += 1
416
+ end
417
+ s2
432
418
  end
433
419
 
434
420
  # The form's authenticity parameter. Override to provide your own.
@@ -441,7 +427,7 @@ module ActionController #:nodoc:
441
427
  allow_forgery_protection
442
428
  end
443
429
 
444
- NULL_ORIGIN_MESSAGE = <<-MSG.strip_heredoc
430
+ NULL_ORIGIN_MESSAGE = <<~MSG
445
431
  The browser returned a 'null' origin for a request with origin-based forgery protection turned on. This usually
446
432
  means you have the 'no-referrer' Referrer-Policy header enabled, or that the request came from a site that
447
433
  refused to give its origin. This makes it impossible for Rails to verify the source of the requests. Likely the