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