actionpack 6.1.4 → 7.0.0.rc1

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 (112) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +189 -372
  3. data/MIT-LICENSE +1 -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 +21 -7
  10. data/lib/abstract_controller/collector.rb +4 -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 +3 -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 +4 -3
  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/helpers.rb +1 -1
  26. data/lib/action_controller/metal/http_authentication.rb +17 -16
  27. data/lib/action_controller/metal/instrumentation.rb +57 -52
  28. data/lib/action_controller/metal/live.rb +42 -2
  29. data/lib/action_controller/metal/mime_responds.rb +3 -3
  30. data/lib/action_controller/metal/params_wrapper.rb +20 -11
  31. data/lib/action_controller/metal/permissions_policy.rb +1 -1
  32. data/lib/action_controller/metal/redirecting.rb +86 -16
  33. data/lib/action_controller/metal/rendering.rb +7 -7
  34. data/lib/action_controller/metal/request_forgery_protection.rb +64 -24
  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 +84 -47
  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 +49 -6
  41. data/lib/action_controller/test_case.rb +19 -4
  42. data/lib/action_controller.rb +1 -5
  43. data/lib/action_dispatch/http/cache.rb +13 -6
  44. data/lib/action_dispatch/http/content_security_policy.rb +39 -35
  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 +1 -13
  52. data/lib/action_dispatch/http/url.rb +11 -19
  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 +6 -13
  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 +8 -4
  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 +3 -0
  70. data/lib/action_dispatch/middleware/flash.rb +9 -11
  71. data/lib/action_dispatch/middleware/host_authorization.rb +49 -37
  72. data/lib/action_dispatch/middleware/remote_ip.rb +16 -4
  73. data/lib/action_dispatch/middleware/server_timing.rb +33 -0
  74. data/lib/action_dispatch/middleware/session/abstract_store.rb +1 -1
  75. data/lib/action_dispatch/middleware/show_exceptions.rb +17 -9
  76. data/lib/action_dispatch/middleware/stack.rb +27 -9
  77. data/lib/action_dispatch/middleware/static.rb +2 -6
  78. data/lib/action_dispatch/middleware/templates/rescues/_message_and_suggestions.html.erb +1 -1
  79. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb +4 -11
  80. data/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +2 -2
  81. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb +4 -3
  82. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.text.erb +3 -1
  83. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +4 -4
  84. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +3 -3
  85. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +28 -18
  86. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb +3 -3
  87. data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +3 -3
  88. data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +3 -3
  89. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +3 -3
  90. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +3 -3
  91. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +5 -14
  92. data/lib/action_dispatch/railtie.rb +8 -2
  93. data/lib/action_dispatch/request/session.rb +43 -13
  94. data/lib/action_dispatch/routing/inspector.rb +1 -1
  95. data/lib/action_dispatch/routing/mapper.rb +54 -78
  96. data/lib/action_dispatch/routing/redirection.rb +0 -2
  97. data/lib/action_dispatch/routing/route_set.rb +14 -6
  98. data/lib/action_dispatch/routing/routes_proxy.rb +1 -1
  99. data/lib/action_dispatch/routing/url_for.rb +1 -2
  100. data/lib/action_dispatch/routing.rb +2 -2
  101. data/lib/action_dispatch/system_test_case.rb +12 -6
  102. data/lib/action_dispatch/system_testing/browser.rb +2 -12
  103. data/lib/action_dispatch/system_testing/driver.rb +35 -11
  104. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +10 -6
  105. data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +0 -8
  106. data/lib/action_dispatch/testing/assertions.rb +2 -5
  107. data/lib/action_dispatch/testing/integration.rb +6 -8
  108. data/lib/action_dispatch/testing/test_process.rb +3 -26
  109. data/lib/action_dispatch.rb +2 -1
  110. data/lib/action_pack/gem_version.rb +4 -4
  111. data/lib/action_pack.rb +1 -1
  112. metadata +19 -17
@@ -2,11 +2,12 @@
2
2
 
3
3
  require "base64"
4
4
  require "active_support/security_utils"
5
+ require "active_support/core_ext/array/access"
5
6
 
6
7
  module ActionController
7
- # Makes it dead easy to do HTTP Basic, Digest and Token authentication.
8
+ # HTTP Basic, Digest and Token authentication.
8
9
  module HttpAuthentication
9
- # Makes it dead easy to do HTTP \Basic authentication.
10
+ # HTTP \Basic authentication.
10
11
  #
11
12
  # === Simple \Basic example
12
13
  #
@@ -24,8 +25,8 @@ module ActionController
24
25
  #
25
26
  # === Advanced \Basic example
26
27
  #
27
- # Here is a more advanced \Basic example where only Atom feeds and the XML API is protected by HTTP authentication,
28
- # the regular HTML interface is protected by a session approach:
28
+ # Here is a more advanced \Basic example where only Atom feeds and the XML API are protected by HTTP authentication.
29
+ # The regular HTML interface is protected by a session approach:
29
30
  #
30
31
  # class ApplicationController < ActionController::Base
31
32
  # before_action :set_account, :authenticate
@@ -103,7 +104,7 @@ module ActionController
103
104
  end
104
105
 
105
106
  def has_basic_credentials?(request)
106
- request.authorization.present? && (auth_scheme(request).downcase == "basic")
107
+ request.authorization.present? && (auth_scheme(request).downcase == "basic") && user_name_and_password(request).length == 2
107
108
  end
108
109
 
109
110
  def user_name_and_password(request)
@@ -134,15 +135,15 @@ module ActionController
134
135
  end
135
136
  end
136
137
 
137
- # Makes it dead easy to do HTTP \Digest authentication.
138
+ # HTTP \Digest authentication.
138
139
  #
139
140
  # === Simple \Digest example
140
141
  #
141
- # require "digest/md5"
142
+ # require "openssl"
142
143
  # class PostsController < ApplicationController
143
144
  # REALM = "SuperSecret"
144
145
  # USERS = {"dhh" => "secret", #plain text password
145
- # "dap" => Digest::MD5.hexdigest(["dap",REALM,"secret"].join(":"))} #ha1 digest password
146
+ # "dap" => OpenSSL::Digest::MD5.hexdigest(["dap",REALM,"secret"].join(":"))} #ha1 digest password
146
147
  #
147
148
  # before_action :authenticate, except: [:index]
148
149
  #
@@ -230,12 +231,12 @@ module ActionController
230
231
  # of a plain-text password.
231
232
  def expected_response(http_method, uri, credentials, password, password_is_ha1 = true)
232
233
  ha1 = password_is_ha1 ? password : ha1(credentials, password)
233
- ha2 = ::Digest::MD5.hexdigest([http_method.to_s.upcase, uri].join(":"))
234
- ::Digest::MD5.hexdigest([ha1, credentials[:nonce], credentials[:nc], credentials[:cnonce], credentials[:qop], ha2].join(":"))
234
+ ha2 = OpenSSL::Digest::MD5.hexdigest([http_method.to_s.upcase, uri].join(":"))
235
+ OpenSSL::Digest::MD5.hexdigest([ha1, credentials[:nonce], credentials[:nc], credentials[:cnonce], credentials[:qop], ha2].join(":"))
235
236
  end
236
237
 
237
238
  def ha1(credentials, password)
238
- ::Digest::MD5.hexdigest([credentials[:username], credentials[:realm], password].join(":"))
239
+ OpenSSL::Digest::MD5.hexdigest([credentials[:username], credentials[:realm], password].join(":"))
239
240
  end
240
241
 
241
242
  def encode_credentials(http_method, credentials, password, password_is_ha1)
@@ -309,7 +310,7 @@ module ActionController
309
310
  def nonce(secret_key, time = Time.now)
310
311
  t = time.to_i
311
312
  hashed = [t, secret_key]
312
- digest = ::Digest::MD5.hexdigest(hashed.join(":"))
313
+ digest = OpenSSL::Digest::MD5.hexdigest(hashed.join(":"))
313
314
  ::Base64.strict_encode64("#{t}:#{digest}")
314
315
  end
315
316
 
@@ -326,11 +327,11 @@ module ActionController
326
327
 
327
328
  # Opaque based on digest of secret key
328
329
  def opaque(secret_key)
329
- ::Digest::MD5.hexdigest(secret_key)
330
+ OpenSSL::Digest::MD5.hexdigest(secret_key)
330
331
  end
331
332
  end
332
333
 
333
- # Makes it dead easy to do HTTP Token authentication.
334
+ # HTTP Token authentication.
334
335
  #
335
336
  # Simple Token example:
336
337
  #
@@ -358,8 +359,8 @@ module ActionController
358
359
  # end
359
360
  #
360
361
  #
361
- # Here is a more advanced Token example where only Atom feeds and the XML API is protected by HTTP token authentication,
362
- # the regular HTML interface is protected by a session approach:
362
+ # Here is a more advanced Token example where only Atom feeds and the XML API are protected by HTTP token authentication.
363
+ # The regular HTML interface is protected by a session approach:
363
364
  #
364
365
  # class ApplicationController < ActionController::Base
365
366
  # before_action :set_account, :authenticate
@@ -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,66 @@ 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
+ ActiveSupport::ExecutionContext[:controller] = self
78
52
 
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
53
+ raw_payload = {
54
+ controller: self.class.name,
55
+ action: action_name,
56
+ request: request,
57
+ params: request.filtered_parameters,
58
+ headers: request.headers,
59
+ format: request.format.ref,
60
+ method: request.request_method,
61
+ path: request.fullpath
62
+ }
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("start_processing.action_controller", raw_payload)
94
65
 
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
66
+ ActiveSupport::Notifications.instrument("process_action.action_controller", raw_payload) do |payload|
67
+ result = super
68
+ payload[:response] = response
69
+ payload[:status] = response.status
70
+ result
71
+ rescue => error
72
+ payload[:status] = ActionDispatch::ExceptionWrapper.status_code_for_exception(error.class.name)
73
+ raise
74
+ ensure
75
+ append_info_to_payload(payload)
76
+ end
77
+ end
78
+
79
+ # A hook invoked every time a before callback is halted.
80
+ def halted_callback_hook(filter, _)
81
+ ActiveSupport::Notifications.instrument("halted_callback.action_controller", filter: filter)
82
+ end
83
+
84
+ # A hook which allows you to clean up any time, wrongly taken into account in
85
+ # views, like database querying time.
86
+ #
87
+ # def cleanup_view_runtime
88
+ # super - time_taken_in_something_expensive
89
+ # end
90
+ def cleanup_view_runtime # :doc:
91
+ yield
92
+ end
93
+
94
+ # Every time after an action is processed, this method is invoked
95
+ # with the payload, so you can add more information.
96
+ def append_info_to_payload(payload) # :doc:
97
+ payload[:view_runtime] = view_runtime
98
+ end
99
+
100
+ module ClassMethods
101
+ # A hook which allows other frameworks to log what happened during
102
+ # controller process action. This method should return an array
103
+ # with the messages to be added.
104
+ def log_process_action(payload) # :nodoc:
105
+ messages, view_runtime = [], payload[:view_runtime]
106
+ messages << ("Views: %.1fms" % view_runtime.to_f) if view_runtime
107
+ messages
108
+ end
103
109
  end
104
- end
105
110
  end
106
111
  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 = {}
@@ -9,11 +9,14 @@ module ActionController
9
9
  # Wraps the parameters hash into a nested hash. This will allow clients to
10
10
  # submit requests without having to specify any root elements.
11
11
  #
12
- # This functionality is enabled in +config/initializers/wrap_parameters.rb+
13
- # and can be customized.
12
+ # This functionality is enabled by default for JSON, and can be customized by
13
+ # setting the format array:
14
14
  #
15
- # You could also turn it on per controller by setting the format array to
16
- # a non-empty array:
15
+ # class ApplicationController < ActionController::Base
16
+ # wrap_parameters format: [:json, :xml]
17
+ # end
18
+ #
19
+ # You could also turn it on per controller:
17
20
  #
18
21
  # class UsersController < ApplicationController
19
22
  # wrap_parameters format: [:json, :xml, :url_encoded_form, :multipart_form]
@@ -68,6 +71,12 @@ module ActionController
68
71
  # will try to check if <tt>Admin::User</tt> or +User+ model exists, and use it to
69
72
  # determine the wrapper key respectively. If both models don't exist,
70
73
  # it will then fallback to use +user+ as the key.
74
+ #
75
+ # To disable this functionality for a controller:
76
+ #
77
+ # class UsersController < ApplicationController
78
+ # wrap_parameters false
79
+ # end
71
80
  module ParamsWrapper
72
81
  extend ActiveSupport::Concern
73
82
 
@@ -242,14 +251,14 @@ module ActionController
242
251
  end
243
252
  end
244
253
 
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
254
  private
255
+ # Performs parameters wrapping upon the request. Called automatically
256
+ # by the metal call stack.
257
+ def process_action(*)
258
+ _perform_parameter_wrapping if _wrapper_enabled?
259
+ super
260
+ end
261
+
253
262
  # Returns the wrapper key which will be used to store wrapped parameters.
254
263
  def _wrapper_key
255
264
  _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.
@@ -7,6 +7,12 @@ module ActionController
7
7
  include AbstractController::Logger
8
8
  include ActionController::UrlFor
9
9
 
10
+ class UnsafeRedirectError < StandardError; end
11
+
12
+ included do
13
+ mattr_accessor :raise_on_open_redirects, default: false
14
+ end
15
+
10
16
  # Redirects the browser to the target specified in +options+. This parameter can be any one of:
11
17
  #
12
18
  # * <tt>Hash</tt> - The URL will be generated by calling url_for with the +options+.
@@ -54,16 +60,42 @@ module ActionController
54
60
  #
55
61
  # Statements after +redirect_to+ in our controller get executed, so +redirect_to+ doesn't stop the execution of the function.
56
62
  # To terminate the execution of the function immediately after the +redirect_to+, use return.
63
+ #
57
64
  # redirect_to post_url(@post) and return
65
+ #
66
+ # === Open Redirect protection
67
+ #
68
+ # By default, Rails protects against redirecting to external hosts for your app's safety, so called open redirects.
69
+ # 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
+ #
71
+ # Here #redirect_to automatically validates the potentially-unsafe URL:
72
+ #
73
+ # redirect_to params[:redirect_url]
74
+ #
75
+ # Raises UnsafeRedirectError in the case of an unsafe redirect.
76
+ #
77
+ # To allow any external redirects pass `allow_other_host: true`, though using a user-provided param in that case is unsafe.
78
+ #
79
+ # redirect_to "https://rubyonrails.org", allow_other_host: true
80
+ #
81
+ # See #url_from for more information on what an internal and safe URL is, or how to fall back to an alternate redirect URL in the unsafe case.
58
82
  def redirect_to(options = {}, response_options = {})
59
83
  raise ActionControllerError.new("Cannot redirect to nil!") unless options
60
84
  raise AbstractController::DoubleRenderError if response_body
61
85
 
86
+ allow_other_host = response_options.delete(:allow_other_host) { _allow_other_host }
87
+
62
88
  self.status = _extract_redirect_to_status(options, response_options)
63
- self.location = _compute_redirect_to_location(request, options)
89
+ self.location = _enforce_open_redirect_protection(_compute_redirect_to_location(request, options), allow_other_host: allow_other_host)
64
90
  self.response_body = "<html><body>You are being <a href=\"#{ERB::Util.unwrapped_html_escape(response.location)}\">redirected</a>.</body></html>"
65
91
  end
66
92
 
93
+ # Soft deprecated alias for #redirect_back_or_to where the +fallback_location+ location is supplied as a keyword argument instead
94
+ # of the first positional argument.
95
+ def redirect_back(fallback_location:, allow_other_host: _allow_other_host, **args)
96
+ redirect_back_or_to fallback_location, allow_other_host: allow_other_host, **args
97
+ end
98
+
67
99
  # Redirects the browser to the page that issued the request (the referrer)
68
100
  # if possible, otherwise redirects to the provided default fallback
69
101
  # location.
@@ -73,35 +105,37 @@ module ActionController
73
105
  # subject to browser security settings and user preferences. If the request
74
106
  # is missing this header, the <tt>fallback_location</tt> will be used.
75
107
  #
76
- # redirect_back fallback_location: { action: "show", id: 5 }
77
- # redirect_back fallback_location: @post
78
- # redirect_back fallback_location: "http://www.rubyonrails.org"
79
- # redirect_back fallback_location: "/images/screenshot.jpg"
80
- # redirect_back fallback_location: posts_url
81
- # redirect_back fallback_location: proc { edit_post_url(@post) }
82
- # redirect_back fallback_location: '/', allow_other_host: false
108
+ # redirect_back_or_to({ action: "show", id: 5 })
109
+ # redirect_back_or_to @post
110
+ # redirect_back_or_to "http://www.rubyonrails.org"
111
+ # redirect_back_or_to "/images/screenshot.jpg"
112
+ # redirect_back_or_to posts_url
113
+ # redirect_back_or_to proc { edit_post_url(@post) }
114
+ # redirect_back_or_to '/', allow_other_host: false
83
115
  #
84
116
  # ==== Options
85
- # * <tt>:fallback_location</tt> - The default fallback location that will be used on missing +Referer+ header.
86
117
  # * <tt>:allow_other_host</tt> - Allow or disallow redirection to the host that is different to the current host, defaults to true.
87
118
  #
88
119
  # All other options that can be passed to #redirect_to are accepted as
89
120
  # options and the behavior is identical.
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
121
+ def redirect_back_or_to(fallback_location, allow_other_host: _allow_other_host, **options)
122
+ if request.referer && (allow_other_host || _url_host_allowed?(request.referer))
123
+ redirect_to request.referer, allow_other_host: allow_other_host, **options
124
+ else
125
+ # The method level `allow_other_host` doesn't apply in the fallback case, omit and let the `redirect_to` handling take over.
126
+ redirect_to fallback_location, **options
127
+ end
94
128
  end
95
129
 
96
- def _compute_redirect_to_location(request, options) #:nodoc:
130
+ def _compute_redirect_to_location(request, options) # :nodoc:
97
131
  case options
98
132
  # The scheme name consist of a letter followed by any combination of
99
133
  # letters, digits, and the plus ("+"), period ("."), or hyphen ("-")
100
134
  # characters; and is terminated by a colon (":").
101
135
  # See https://tools.ietf.org/html/rfc3986#section-3.1
102
136
  # The protocol relative scheme starts with a double slash "//".
103
- when /\A([a-z][a-z\d\-+\.]*:|\/\/).*/i
104
- options
137
+ when /\A([a-z][a-z\d\-+.]*:|\/\/).*/i
138
+ options.to_str
105
139
  when String
106
140
  request.protocol + request.host_with_port + options
107
141
  when Proc
@@ -113,7 +147,35 @@ module ActionController
113
147
  module_function :_compute_redirect_to_location
114
148
  public :_compute_redirect_to_location
115
149
 
150
+ # Verifies the passed +location+ is an internal URL that's safe to redirect to and returns it, or nil if not.
151
+ # Useful to wrap a params provided redirect URL and fallback to an alternate URL to redirect to:
152
+ #
153
+ # redirect_to url_from(params[:redirect_url]) || root_url
154
+ #
155
+ # The +location+ is considered internal, and safe, if it's on the same host as <tt>request.host</tt>:
156
+ #
157
+ # # If request.host is example.com:
158
+ # url_from("https://example.com/profile") # => "https://example.com/profile"
159
+ # url_from("http://example.com/profile") # => "http://example.com/profile"
160
+ # url_from("http://evil.com/profile") # => nil
161
+ #
162
+ # Subdomains are considered part of the host:
163
+ #
164
+ # # If request.host is on https://example.com or https://app.example.com, you'd get:
165
+ # url_from("https://dev.example.com/profile") # => nil
166
+ #
167
+ # NOTE: there's a similarity with {url_for}[rdoc-ref:ActionDispatch::Routing::UrlFor#url_for], which generates an internal URL from various options from within the app, e.g. <tt>url_for(@post)</tt>.
168
+ # However, #url_from is meant to take an external parameter to verify as in <tt>url_from(params[:redirect_url])</tt>.
169
+ def url_from(location)
170
+ location = location.presence
171
+ location if location && _url_host_allowed?(location)
172
+ end
173
+
116
174
  private
175
+ def _allow_other_host
176
+ !raise_on_open_redirects
177
+ end
178
+
117
179
  def _extract_redirect_to_status(options, response_options)
118
180
  if options.is_a?(Hash) && options.key?(:status)
119
181
  Rack::Utils.status_code(options.delete(:status))
@@ -124,6 +186,14 @@ module ActionController
124
186
  end
125
187
  end
126
188
 
189
+ def _enforce_open_redirect_protection(location, allow_other_host:)
190
+ if allow_other_host || _url_host_allowed?(location)
191
+ location
192
+ else
193
+ raise UnsafeRedirectError, "Unsafe redirect to #{location.truncate(100).inspect}, pass allow_other_host: true to redirect anyway."
194
+ end
195
+ end
196
+
127
197
  def _url_host_allowed?(url)
128
198
  URI(url.to_s).host == request.host
129
199
  rescue ArgumentError, URI::Error
@@ -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