actionpack 5.2.1 → 7.0.2.4
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 +264 -220
- data/MIT-LICENSE +1 -1
- data/README.rdoc +6 -6
- data/lib/abstract_controller/asset_paths.rb +1 -1
- data/lib/abstract_controller/base.rb +24 -4
- data/lib/abstract_controller/caching/fragments.rb +8 -24
- data/lib/abstract_controller/caching.rb +2 -2
- data/lib/abstract_controller/callbacks.rb +34 -8
- data/lib/abstract_controller/collector.rb +5 -4
- data/lib/abstract_controller/error.rb +1 -1
- data/lib/abstract_controller/helpers.rb +107 -90
- data/lib/abstract_controller/logger.rb +1 -1
- data/lib/abstract_controller/railties/routes_helpers.rb +19 -1
- data/lib/abstract_controller/rendering.rb +9 -9
- data/lib/abstract_controller/translation.rb +12 -5
- data/lib/abstract_controller/url_for.rb +4 -6
- data/lib/abstract_controller.rb +2 -0
- data/lib/action_controller/api.rb +5 -4
- data/lib/action_controller/base.rb +6 -9
- data/lib/action_controller/caching.rb +1 -3
- data/lib/action_controller/log_subscriber.rb +13 -9
- data/lib/action_controller/metal/basic_implicit_render.rb +1 -1
- data/lib/action_controller/metal/conditional_get.rb +57 -6
- data/lib/action_controller/metal/content_security_policy.rb +2 -3
- data/lib/action_controller/metal/cookies.rb +4 -2
- data/lib/action_controller/metal/data_streaming.rb +9 -18
- data/lib/action_controller/metal/default_headers.rb +17 -0
- data/lib/action_controller/metal/etag_with_template_digest.rb +4 -6
- data/lib/action_controller/metal/exceptions.rb +55 -12
- data/lib/action_controller/metal/flash.rb +10 -6
- data/lib/action_controller/metal/head.rb +7 -4
- data/lib/action_controller/metal/helpers.rb +15 -6
- data/lib/action_controller/metal/http_authentication.rb +41 -39
- data/lib/action_controller/metal/implicit_render.rb +5 -15
- data/lib/action_controller/metal/instrumentation.rb +59 -55
- data/lib/action_controller/metal/live.rb +80 -33
- data/lib/action_controller/metal/logging.rb +20 -0
- data/lib/action_controller/metal/mime_responds.rb +22 -7
- data/lib/action_controller/metal/parameter_encoding.rb +35 -4
- data/lib/action_controller/metal/params_wrapper.rb +50 -31
- data/lib/action_controller/metal/permissions_policy.rb +46 -0
- data/lib/action_controller/metal/redirecting.rb +93 -23
- data/lib/action_controller/metal/renderers.rb +4 -4
- data/lib/action_controller/metal/rendering.rb +14 -9
- data/lib/action_controller/metal/request_forgery_protection.rb +160 -58
- data/lib/action_controller/metal/rescue.rb +2 -2
- data/lib/action_controller/metal/streaming.rb +1 -4
- data/lib/action_controller/metal/strong_parameters.rb +236 -88
- data/lib/action_controller/metal/testing.rb +9 -2
- data/lib/action_controller/metal/url_for.rb +1 -1
- data/lib/action_controller/metal.rb +16 -17
- data/lib/action_controller/railtie.rb +49 -6
- data/lib/action_controller/railties/helpers.rb +1 -1
- data/lib/action_controller/renderer.rb +37 -13
- data/lib/action_controller/template_assertions.rb +1 -1
- data/lib/action_controller/test_case.rb +98 -68
- data/lib/action_controller.rb +4 -5
- data/lib/action_dispatch/http/cache.rb +45 -32
- data/lib/action_dispatch/http/content_disposition.rb +45 -0
- data/lib/action_dispatch/http/content_security_policy.rb +69 -56
- data/lib/action_dispatch/http/filter_parameters.rb +14 -8
- data/lib/action_dispatch/http/filter_redirect.rb +2 -3
- data/lib/action_dispatch/http/headers.rb +4 -4
- data/lib/action_dispatch/http/mime_negotiation.rb +44 -16
- data/lib/action_dispatch/http/mime_type.rb +47 -30
- data/lib/action_dispatch/http/parameters.rb +18 -27
- data/lib/action_dispatch/http/permissions_policy.rb +173 -0
- data/lib/action_dispatch/http/request.rb +49 -35
- data/lib/action_dispatch/http/response.rb +34 -26
- data/lib/action_dispatch/http/upload.rb +9 -1
- data/lib/action_dispatch/http/url.rb +86 -94
- data/lib/action_dispatch/journey/formatter.rb +55 -31
- data/lib/action_dispatch/journey/gtg/builder.rb +30 -46
- data/lib/action_dispatch/journey/gtg/simulator.rb +15 -8
- data/lib/action_dispatch/journey/gtg/transition_table.rb +78 -21
- data/lib/action_dispatch/journey/nfa/dot.rb +0 -11
- data/lib/action_dispatch/journey/nodes/node.rb +83 -16
- data/lib/action_dispatch/journey/parser.rb +13 -13
- data/lib/action_dispatch/journey/parser.y +1 -1
- data/lib/action_dispatch/journey/path/pattern.rb +42 -34
- data/lib/action_dispatch/journey/route.rb +14 -31
- data/lib/action_dispatch/journey/router/utils.rb +16 -14
- data/lib/action_dispatch/journey/router.rb +27 -35
- data/lib/action_dispatch/journey/routes.rb +3 -5
- data/lib/action_dispatch/journey/scanner.rb +10 -4
- data/lib/action_dispatch/journey/visitors.rb +1 -4
- data/lib/action_dispatch/journey/visualizer/fsm.js +49 -24
- data/lib/action_dispatch/journey/visualizer/index.html.erb +1 -1
- data/lib/action_dispatch/journey.rb +0 -2
- data/lib/action_dispatch/middleware/actionable_exceptions.rb +45 -0
- data/lib/action_dispatch/middleware/callbacks.rb +2 -4
- data/lib/action_dispatch/middleware/cookies.rb +136 -113
- data/lib/action_dispatch/middleware/debug_exceptions.rb +47 -68
- data/lib/action_dispatch/middleware/debug_locks.rb +8 -8
- data/lib/action_dispatch/middleware/debug_view.rb +66 -0
- data/lib/action_dispatch/middleware/exception_wrapper.rb +79 -30
- data/lib/action_dispatch/middleware/executor.rb +4 -1
- data/lib/action_dispatch/middleware/flash.rb +10 -12
- data/lib/action_dispatch/middleware/host_authorization.rb +159 -0
- data/lib/action_dispatch/middleware/public_exceptions.rb +6 -3
- data/lib/action_dispatch/middleware/remote_ip.rb +30 -20
- data/lib/action_dispatch/middleware/request_id.rb +5 -6
- data/lib/action_dispatch/middleware/server_timing.rb +33 -0
- data/lib/action_dispatch/middleware/session/abstract_store.rb +16 -3
- data/lib/action_dispatch/middleware/session/cache_store.rb +11 -6
- data/lib/action_dispatch/middleware/session/cookie_store.rb +24 -19
- data/lib/action_dispatch/middleware/show_exceptions.rb +20 -11
- data/lib/action_dispatch/middleware/ssl.rb +20 -15
- data/lib/action_dispatch/middleware/stack.rb +79 -7
- data/lib/action_dispatch/middleware/static.rb +150 -94
- data/lib/action_dispatch/middleware/templates/rescues/_actions.html.erb +13 -0
- data/lib/action_dispatch/middleware/templates/rescues/_actions.text.erb +0 -0
- data/lib/action_dispatch/middleware/templates/rescues/_message_and_suggestions.html.erb +22 -0
- data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb +6 -11
- data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.text.erb +1 -1
- data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +4 -2
- data/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +46 -36
- data/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb +8 -0
- data/lib/action_dispatch/middleware/templates/rescues/blocked_host.text.erb +7 -0
- data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +25 -6
- data/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb +1 -1
- data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +9 -6
- data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +4 -1
- data/lib/action_dispatch/middleware/templates/rescues/layout.erb +121 -15
- data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb +19 -0
- data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.text.erb +3 -0
- data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +5 -5
- data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +4 -4
- data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +5 -5
- data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +4 -4
- data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +16 -2
- data/lib/action_dispatch/railtie.rb +16 -4
- data/lib/action_dispatch/request/session.rb +59 -22
- data/lib/action_dispatch/request/utils.rb +28 -2
- data/lib/action_dispatch/routing/inspector.rb +102 -54
- data/lib/action_dispatch/routing/mapper.rb +184 -156
- data/lib/action_dispatch/routing/polymorphic_routes.rb +21 -19
- data/lib/action_dispatch/routing/redirection.rb +4 -6
- data/lib/action_dispatch/routing/route_set.rb +83 -73
- data/lib/action_dispatch/routing/routes_proxy.rb +1 -1
- data/lib/action_dispatch/routing/url_for.rb +2 -3
- data/lib/action_dispatch/routing.rb +23 -22
- data/lib/action_dispatch/system_test_case.rb +65 -16
- data/lib/action_dispatch/system_testing/browser.rb +43 -16
- data/lib/action_dispatch/system_testing/driver.rb +42 -10
- data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +58 -12
- data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +3 -10
- data/lib/action_dispatch/testing/assertion_response.rb +0 -1
- data/lib/action_dispatch/testing/assertions/response.rb +4 -7
- data/lib/action_dispatch/testing/assertions/routing.rb +20 -8
- data/lib/action_dispatch/testing/assertions.rb +3 -6
- data/lib/action_dispatch/testing/integration.rb +61 -30
- data/lib/action_dispatch/testing/request_encoder.rb +2 -2
- data/lib/action_dispatch/testing/test_process.rb +8 -6
- data/lib/action_dispatch/testing/test_request.rb +3 -3
- data/lib/action_dispatch/testing/test_response.rb +4 -32
- data/lib/action_dispatch.rb +15 -7
- data/lib/action_pack/gem_version.rb +4 -4
- data/lib/action_pack.rb +1 -1
- metadata +44 -25
- data/lib/action_controller/metal/force_ssl.rb +0 -99
- data/lib/action_dispatch/http/parameter_filter.rb +0 -86
- data/lib/action_dispatch/journey/nfa/builder.rb +0 -78
- data/lib/action_dispatch/journey/nfa/simulator.rb +0 -49
- data/lib/action_dispatch/journey/nfa/transition_table.rb +0 -120
- data/lib/action_dispatch/system_testing/test_helpers/undef_methods.rb +0 -26
@@ -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
|
58
|
-
|
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.
|
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
|
|
62
|
-
|
63
|
-
|
86
|
+
allow_other_host = response_options.delete(:allow_other_host) { _allow_other_host }
|
87
|
+
|
88
|
+
self.status = _extract_redirect_to_status(options, response_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,39 +105,41 @@ 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
|
-
#
|
77
|
-
#
|
78
|
-
#
|
79
|
-
#
|
80
|
-
#
|
81
|
-
#
|
82
|
-
#
|
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
|
-
# All other options that can be passed to
|
119
|
+
# All other options that can be passed to #redirect_to are accepted as
|
89
120
|
# options and the behavior is identical.
|
90
|
-
def
|
91
|
-
referer
|
92
|
-
|
93
|
-
|
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)
|
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
|
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
|
108
|
-
_compute_redirect_to_location request, options
|
142
|
+
_compute_redirect_to_location request, instance_eval(&options)
|
109
143
|
else
|
110
144
|
url_for(options)
|
111
145
|
end.delete("\0\r\n")
|
@@ -113,17 +147,53 @@ 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
|
117
|
-
def
|
175
|
+
def _allow_other_host
|
176
|
+
!raise_on_open_redirects
|
177
|
+
end
|
178
|
+
|
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))
|
120
|
-
elsif
|
121
|
-
Rack::Utils.status_code(
|
182
|
+
elsif response_options.key?(:status)
|
183
|
+
Rack::Utils.status_code(response_options[:status])
|
122
184
|
else
|
123
185
|
302
|
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
|
@@ -157,24 +157,24 @@ module ActionController
|
|
157
157
|
json = json.to_json(options) unless json.kind_of?(String)
|
158
158
|
|
159
159
|
if options[:callback].present?
|
160
|
-
if
|
160
|
+
if media_type.nil? || media_type == Mime[:json]
|
161
161
|
self.content_type = Mime[:js]
|
162
162
|
end
|
163
163
|
|
164
164
|
"/**/#{options[:callback]}(#{json})"
|
165
165
|
else
|
166
|
-
self.content_type
|
166
|
+
self.content_type = Mime[:json] if media_type.nil?
|
167
167
|
json
|
168
168
|
end
|
169
169
|
end
|
170
170
|
|
171
171
|
add :js do |js, options|
|
172
|
-
self.content_type
|
172
|
+
self.content_type = Mime[:js] if media_type.nil?
|
173
173
|
js.respond_to?(:to_js) ? js.to_js(options) : js
|
174
174
|
end
|
175
175
|
|
176
176
|
add :xml do |xml, options|
|
177
|
-
self.content_type
|
177
|
+
self.content_type = Mime[:xml] if media_type.nil?
|
178
178
|
xml.respond_to?(:to_xml) ? xml.to_xml(options) : xml
|
179
179
|
end
|
180
180
|
end
|
@@ -24,14 +24,8 @@ module ActionController
|
|
24
24
|
end
|
25
25
|
end
|
26
26
|
|
27
|
-
# Before processing, set the request formats in current controller formats.
|
28
|
-
def process_action(*) #:nodoc:
|
29
|
-
self.formats = request.formats.map(&:ref).compact
|
30
|
-
super
|
31
|
-
end
|
32
|
-
|
33
27
|
# Check for double render errors and set the content_type after rendering.
|
34
|
-
def render(*args)
|
28
|
+
def render(*args) # :nodoc:
|
35
29
|
raise ::AbstractController::DoubleRenderError if response_body
|
36
30
|
super
|
37
31
|
end
|
@@ -40,7 +34,7 @@ module ActionController
|
|
40
34
|
def render_to_string(*)
|
41
35
|
result = super
|
42
36
|
if result.respond_to?(:each)
|
43
|
-
string = ""
|
37
|
+
string = +""
|
44
38
|
result.each { |r| string << r }
|
45
39
|
string
|
46
40
|
else
|
@@ -53,6 +47,11 @@ 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
|
56
55
|
|
57
56
|
def _process_variant(options)
|
58
57
|
if defined?(request) && !request.nil? && request.variant.present?
|
@@ -73,11 +72,17 @@ module ActionController
|
|
73
72
|
end
|
74
73
|
|
75
74
|
def _set_rendered_content_type(format)
|
76
|
-
if format && !response.
|
75
|
+
if format && !response.media_type
|
77
76
|
self.content_type = format.to_s
|
78
77
|
end
|
79
78
|
end
|
80
79
|
|
80
|
+
def _set_vary_header
|
81
|
+
if self.headers["Vary"].blank? && request.should_apply_vary_header?
|
82
|
+
self.headers["Vary"] = "Accept"
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
81
86
|
# Normalize arguments by catching blocks and setting them on :update.
|
82
87
|
def _normalize_args(action = nil, options = {}, &blk)
|
83
88
|
options = super
|
@@ -3,13 +3,12 @@
|
|
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
|
-
module ActionController
|
9
|
-
class InvalidAuthenticityToken < ActionControllerError
|
7
|
+
module ActionController # :nodoc:
|
8
|
+
class InvalidAuthenticityToken < ActionControllerError # :nodoc:
|
10
9
|
end
|
11
10
|
|
12
|
-
class InvalidCrossOriginRequest < ActionControllerError
|
11
|
+
class InvalidCrossOriginRequest < ActionControllerError # :nodoc:
|
13
12
|
end
|
14
13
|
|
15
14
|
# Controller actions are protected from Cross-Site Request Forgery (CSRF) attacks
|
@@ -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
|
-
#
|
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,31 +30,30 @@ 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
|
33
|
+
# Ajax) requests are allowed to make requests for JavaScript responses.
|
35
34
|
#
|
36
|
-
#
|
37
|
-
#
|
38
|
-
# <tt>
|
35
|
+
# Subclasses of <tt>ActionController::Base</tt> are protected by default with the
|
36
|
+
# <tt>:exception</tt> strategy, which raises an
|
37
|
+
# <tt>ActionController::InvalidAuthenticityToken</tt> error on unverified requests.
|
38
|
+
#
|
39
|
+
# 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.
|
41
|
+
# One way to achieve this is to use the <tt>:null_session</tt> strategy instead,
|
42
|
+
# which allows unverified requests to be handled, but with an empty session:
|
39
43
|
#
|
40
44
|
# class ApplicationController < ActionController::Base
|
41
|
-
# protect_from_forgery
|
45
|
+
# protect_from_forgery with: :null_session
|
42
46
|
# end
|
43
47
|
#
|
44
|
-
#
|
45
|
-
#
|
46
|
-
# <tt>:null_session</tt> method, which provides an empty session
|
47
|
-
# during request.
|
48
|
-
#
|
49
|
-
# We may want to disable CSRF protection for APIs since they are typically
|
50
|
-
# designed to be state-less. That is, the request API client will handle
|
51
|
-
# the session for you instead of Rails.
|
48
|
+
# Note that API only applications don't include this module or a session middleware
|
49
|
+
# by default, and so don't require CSRF protection to be configured.
|
52
50
|
#
|
53
51
|
# The token parameter is named <tt>authenticity_token</tt> by default. The name and
|
54
52
|
# value of this token must be added to every layout that renders forms by including
|
55
53
|
# <tt>csrf_meta_tags</tt> in the HTML +head+.
|
56
54
|
#
|
57
55
|
# Learn more about CSRF attacks and securing your application in the
|
58
|
-
# {Ruby on Rails Security Guide}[
|
56
|
+
# {Ruby on Rails Security Guide}[https://guides.rubyonrails.org/security.html].
|
59
57
|
module RequestForgeryProtection
|
60
58
|
extend ActiveSupport::Concern
|
61
59
|
|
@@ -92,6 +90,19 @@ module ActionController #:nodoc:
|
|
92
90
|
config_accessor :default_protect_from_forgery
|
93
91
|
self.default_protect_from_forgery = false
|
94
92
|
|
93
|
+
# Controls whether URL-safe CSRF tokens are generated.
|
94
|
+
config_accessor :urlsafe_csrf_tokens, instance_writer: false
|
95
|
+
self.urlsafe_csrf_tokens = true
|
96
|
+
|
97
|
+
singleton_class.redefine_method(:urlsafe_csrf_tokens=) do |urlsafe_csrf_tokens|
|
98
|
+
if urlsafe_csrf_tokens
|
99
|
+
ActiveSupport::Deprecation.warn("URL-safe CSRF tokens are now the default. Use 6.1 defaults or above.")
|
100
|
+
else
|
101
|
+
ActiveSupport::Deprecation.warn("Non-URL-safe CSRF tokens are deprecated. Use 6.1 defaults or above.")
|
102
|
+
end
|
103
|
+
config.urlsafe_csrf_tokens = urlsafe_csrf_tokens
|
104
|
+
end
|
105
|
+
|
95
106
|
helper_method :form_authenticity_token
|
96
107
|
helper_method :protect_against_forgery?
|
97
108
|
end
|
@@ -122,10 +133,26 @@ module ActionController #:nodoc:
|
|
122
133
|
# If you need to add verification to the beginning of the callback chain, use <tt>prepend: true</tt>.
|
123
134
|
# * <tt>:with</tt> - Set the method to handle unverified request.
|
124
135
|
#
|
125
|
-
#
|
136
|
+
# Built-in unverified request handling methods are:
|
126
137
|
# * <tt>:exception</tt> - Raises ActionController::InvalidAuthenticityToken exception.
|
127
138
|
# * <tt>:reset_session</tt> - Resets the session.
|
128
139
|
# * <tt>:null_session</tt> - Provides an empty session during request but doesn't reset it completely. Used as default if <tt>:with</tt> option is not specified.
|
140
|
+
#
|
141
|
+
# You can also implement custom strategy classes for unverified request handling:
|
142
|
+
#
|
143
|
+
# class CustomStrategy
|
144
|
+
# def initialize(controller)
|
145
|
+
# @controller = controller
|
146
|
+
# end
|
147
|
+
#
|
148
|
+
# def handle_unverified_request
|
149
|
+
# # Custom behaviour for unverfied request
|
150
|
+
# end
|
151
|
+
# end
|
152
|
+
#
|
153
|
+
# class ApplicationController < ActionController:x:Base
|
154
|
+
# protect_from_forgery with: CustomStrategy
|
155
|
+
# end
|
129
156
|
def protect_from_forgery(options = {})
|
130
157
|
options = options.reverse_merge(prepend: false)
|
131
158
|
|
@@ -145,11 +172,19 @@ module ActionController #:nodoc:
|
|
145
172
|
end
|
146
173
|
|
147
174
|
private
|
148
|
-
|
149
175
|
def protection_method_class(name)
|
150
|
-
|
151
|
-
|
152
|
-
|
176
|
+
case name
|
177
|
+
when :null_session
|
178
|
+
ProtectionMethods::NullSession
|
179
|
+
when :reset_session
|
180
|
+
ProtectionMethods::ResetSession
|
181
|
+
when :exception
|
182
|
+
ProtectionMethods::Exception
|
183
|
+
when Class
|
184
|
+
name
|
185
|
+
else
|
186
|
+
raise ArgumentError, "Invalid request forgery protection method, use :null_session, :exception, :reset_session, or a custom forgery protection class."
|
187
|
+
end
|
153
188
|
end
|
154
189
|
end
|
155
190
|
|
@@ -169,8 +204,7 @@ module ActionController #:nodoc:
|
|
169
204
|
end
|
170
205
|
|
171
206
|
private
|
172
|
-
|
173
|
-
class NullSessionHash < Rack::Session::Abstract::SessionHash #:nodoc:
|
207
|
+
class NullSessionHash < Rack::Session::Abstract::SessionHash # :nodoc:
|
174
208
|
def initialize(req)
|
175
209
|
super(nil, req)
|
176
210
|
@data = {}
|
@@ -183,9 +217,13 @@ module ActionController #:nodoc:
|
|
183
217
|
def exists?
|
184
218
|
true
|
185
219
|
end
|
220
|
+
|
221
|
+
def enabled?
|
222
|
+
false
|
223
|
+
end
|
186
224
|
end
|
187
225
|
|
188
|
-
class NullCookieJar < ActionDispatch::Cookies::CookieJar
|
226
|
+
class NullCookieJar < ActionDispatch::Cookies::CookieJar # :nodoc:
|
189
227
|
def write(*)
|
190
228
|
# nothing
|
191
229
|
end
|
@@ -203,12 +241,14 @@ module ActionController #:nodoc:
|
|
203
241
|
end
|
204
242
|
|
205
243
|
class Exception
|
244
|
+
attr_accessor :warning_message
|
245
|
+
|
206
246
|
def initialize(controller)
|
207
247
|
@controller = controller
|
208
248
|
end
|
209
249
|
|
210
250
|
def handle_unverified_request
|
211
|
-
raise ActionController::InvalidAuthenticityToken
|
251
|
+
raise ActionController::InvalidAuthenticityToken, warning_message
|
212
252
|
end
|
213
253
|
end
|
214
254
|
end
|
@@ -228,22 +268,31 @@ module ActionController #:nodoc:
|
|
228
268
|
mark_for_same_origin_verification!
|
229
269
|
|
230
270
|
if !verified_request?
|
231
|
-
if logger && log_warning_on_csrf_failure
|
232
|
-
|
233
|
-
logger.warn "Can't verify CSRF token authenticity."
|
234
|
-
else
|
235
|
-
logger.warn "HTTP Origin header (#{request.origin}) didn't match request.base_url (#{request.base_url})"
|
236
|
-
end
|
237
|
-
end
|
271
|
+
logger.warn unverified_request_warning_message if logger && log_warning_on_csrf_failure
|
272
|
+
|
238
273
|
handle_unverified_request
|
239
274
|
end
|
240
275
|
end
|
241
276
|
|
242
277
|
def handle_unverified_request # :doc:
|
243
|
-
forgery_protection_strategy.new(self)
|
278
|
+
protection_strategy = forgery_protection_strategy.new(self)
|
279
|
+
|
280
|
+
if protection_strategy.respond_to?(:warning_message)
|
281
|
+
protection_strategy.warning_message = unverified_request_warning_message
|
282
|
+
end
|
283
|
+
|
284
|
+
protection_strategy.handle_unverified_request
|
244
285
|
end
|
245
286
|
|
246
|
-
|
287
|
+
def unverified_request_warning_message # :nodoc:
|
288
|
+
if valid_request_origin?
|
289
|
+
"Can't verify CSRF token authenticity."
|
290
|
+
else
|
291
|
+
"HTTP Origin header (#{request.origin}) didn't match request.base_url (#{request.base_url})"
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
# :nodoc:
|
247
296
|
CROSS_ORIGIN_JAVASCRIPT_WARNING = "Security warning: an embedded " \
|
248
297
|
"<script> tag on another site requested protected JavaScript. " \
|
249
298
|
"If you know what you're doing, go ahead and disable forgery " \
|
@@ -276,7 +325,7 @@ module ActionController #:nodoc:
|
|
276
325
|
|
277
326
|
# Check for cross-origin JavaScript responses.
|
278
327
|
def non_xhr_javascript_response? # :doc:
|
279
|
-
|
328
|
+
%r(\A(?:text|application)/javascript).match?(media_type) && !request.xhr?
|
280
329
|
end
|
281
330
|
|
282
331
|
AUTHENTICITY_TOKEN_LENGTH = 32
|
@@ -303,28 +352,25 @@ module ActionController #:nodoc:
|
|
303
352
|
[form_authenticity_param, request.x_csrf_token]
|
304
353
|
end
|
305
354
|
|
306
|
-
#
|
307
|
-
def form_authenticity_token(form_options: {})
|
355
|
+
# Creates the authenticity token for the current request.
|
356
|
+
def form_authenticity_token(form_options: {}) # :doc:
|
308
357
|
masked_authenticity_token(session, form_options: form_options)
|
309
358
|
end
|
310
359
|
|
311
360
|
# Creates a masked version of the authenticity token that varies
|
312
361
|
# on each request. The masking is used to mitigate SSL attacks
|
313
362
|
# like BREACH.
|
314
|
-
def masked_authenticity_token(session, form_options: {})
|
363
|
+
def masked_authenticity_token(session, form_options: {})
|
315
364
|
action, method = form_options.values_at(:action, :method)
|
316
365
|
|
317
366
|
raw_token = if per_form_csrf_tokens && action && method
|
318
367
|
action_path = normalize_action_path(action)
|
319
368
|
per_form_csrf_token(session, action_path, method)
|
320
369
|
else
|
321
|
-
|
370
|
+
global_csrf_token(session)
|
322
371
|
end
|
323
372
|
|
324
|
-
|
325
|
-
encrypted_csrf_token = xor_byte_strings(one_time_pad, raw_token)
|
326
|
-
masked_token = one_time_pad + encrypted_csrf_token
|
327
|
-
Base64.strict_encode64(masked_token)
|
373
|
+
mask_token(raw_token)
|
328
374
|
end
|
329
375
|
|
330
376
|
# Checks the client's masked token to see if it matches the
|
@@ -336,7 +382,7 @@ module ActionController #:nodoc:
|
|
336
382
|
end
|
337
383
|
|
338
384
|
begin
|
339
|
-
masked_token =
|
385
|
+
masked_token = decode_csrf_token(encoded_masked_token)
|
340
386
|
rescue ArgumentError # encoded_masked_token is invalid Base64
|
341
387
|
return false
|
342
388
|
end
|
@@ -354,7 +400,8 @@ module ActionController #:nodoc:
|
|
354
400
|
elsif masked_token.length == AUTHENTICITY_TOKEN_LENGTH * 2
|
355
401
|
csrf_token = unmask_token(masked_token)
|
356
402
|
|
357
|
-
|
403
|
+
compare_with_global_token(csrf_token, session) ||
|
404
|
+
compare_with_real_token(csrf_token, session) ||
|
358
405
|
valid_per_form_csrf_token?(csrf_token, session)
|
359
406
|
else
|
360
407
|
false # Token is malformed.
|
@@ -369,15 +416,26 @@ module ActionController #:nodoc:
|
|
369
416
|
xor_byte_strings(one_time_pad, encrypted_csrf_token)
|
370
417
|
end
|
371
418
|
|
419
|
+
def mask_token(raw_token) # :doc:
|
420
|
+
one_time_pad = SecureRandom.random_bytes(AUTHENTICITY_TOKEN_LENGTH)
|
421
|
+
encrypted_csrf_token = xor_byte_strings(one_time_pad, raw_token)
|
422
|
+
masked_token = one_time_pad + encrypted_csrf_token
|
423
|
+
encode_csrf_token(masked_token)
|
424
|
+
end
|
425
|
+
|
372
426
|
def compare_with_real_token(token, session) # :doc:
|
373
427
|
ActiveSupport::SecurityUtils.fixed_length_secure_compare(token, real_csrf_token(session))
|
374
428
|
end
|
375
429
|
|
430
|
+
def compare_with_global_token(token, session) # :doc:
|
431
|
+
ActiveSupport::SecurityUtils.fixed_length_secure_compare(token, global_csrf_token(session))
|
432
|
+
end
|
433
|
+
|
376
434
|
def valid_per_form_csrf_token?(token, session) # :doc:
|
377
435
|
if per_form_csrf_tokens
|
378
436
|
correct_token = per_form_csrf_token(
|
379
437
|
session,
|
380
|
-
|
438
|
+
request.path.chomp("/"),
|
381
439
|
request.request_method
|
382
440
|
)
|
383
441
|
|
@@ -388,22 +446,38 @@ module ActionController #:nodoc:
|
|
388
446
|
end
|
389
447
|
|
390
448
|
def real_csrf_token(session) # :doc:
|
391
|
-
session[:_csrf_token] ||=
|
392
|
-
|
449
|
+
session[:_csrf_token] ||= generate_csrf_token
|
450
|
+
decode_csrf_token(session[:_csrf_token])
|
393
451
|
end
|
394
452
|
|
395
453
|
def per_form_csrf_token(session, action_path, method) # :doc:
|
454
|
+
csrf_token_hmac(session, [action_path, method.downcase].join("#"))
|
455
|
+
end
|
456
|
+
|
457
|
+
GLOBAL_CSRF_TOKEN_IDENTIFIER = "!real_csrf_token"
|
458
|
+
private_constant :GLOBAL_CSRF_TOKEN_IDENTIFIER
|
459
|
+
|
460
|
+
def global_csrf_token(session) # :doc:
|
461
|
+
csrf_token_hmac(session, GLOBAL_CSRF_TOKEN_IDENTIFIER)
|
462
|
+
end
|
463
|
+
|
464
|
+
def csrf_token_hmac(session, identifier) # :doc:
|
396
465
|
OpenSSL::HMAC.digest(
|
397
466
|
OpenSSL::Digest::SHA256.new,
|
398
467
|
real_csrf_token(session),
|
399
|
-
|
468
|
+
identifier
|
400
469
|
)
|
401
470
|
end
|
402
471
|
|
403
472
|
def xor_byte_strings(s1, s2) # :doc:
|
404
|
-
|
405
|
-
s1.
|
406
|
-
|
473
|
+
s2 = s2.dup
|
474
|
+
size = s1.bytesize
|
475
|
+
i = 0
|
476
|
+
while i < size
|
477
|
+
s2.setbyte(i, s1.getbyte(i) ^ s2.getbyte(i))
|
478
|
+
i += 1
|
479
|
+
end
|
480
|
+
s2
|
407
481
|
end
|
408
482
|
|
409
483
|
# The form's authenticity parameter. Override to provide your own.
|
@@ -413,14 +487,14 @@ module ActionController #:nodoc:
|
|
413
487
|
|
414
488
|
# Checks if the controller allows forgery protection.
|
415
489
|
def protect_against_forgery? # :doc:
|
416
|
-
allow_forgery_protection
|
490
|
+
allow_forgery_protection && (!session.respond_to?(:enabled?) || session.enabled?)
|
417
491
|
end
|
418
492
|
|
419
|
-
NULL_ORIGIN_MESSAGE =
|
493
|
+
NULL_ORIGIN_MESSAGE = <<~MSG
|
420
494
|
The browser returned a 'null' origin for a request with origin-based forgery protection turned on. This usually
|
421
495
|
means you have the 'no-referrer' Referrer-Policy header enabled, or that the request came from a site that
|
422
496
|
refused to give its origin. This makes it impossible for Rails to verify the source of the requests. Likely the
|
423
|
-
best solution is to change your referrer policy to something less strict like same-origin or strict-
|
497
|
+
best solution is to change your referrer policy to something less strict like same-origin or strict-origin.
|
424
498
|
If you cannot change the referrer policy, you can disable origin checking with the
|
425
499
|
Rails.application.config.action_controller.forgery_protection_origin_check setting.
|
426
500
|
MSG
|
@@ -441,5 +515,33 @@ module ActionController #:nodoc:
|
|
441
515
|
uri = URI.parse(action_path)
|
442
516
|
uri.path.chomp("/")
|
443
517
|
end
|
518
|
+
|
519
|
+
def generate_csrf_token # :nodoc:
|
520
|
+
if urlsafe_csrf_tokens
|
521
|
+
SecureRandom.urlsafe_base64(AUTHENTICITY_TOKEN_LENGTH)
|
522
|
+
else
|
523
|
+
SecureRandom.base64(AUTHENTICITY_TOKEN_LENGTH)
|
524
|
+
end
|
525
|
+
end
|
526
|
+
|
527
|
+
def encode_csrf_token(csrf_token) # :nodoc:
|
528
|
+
if urlsafe_csrf_tokens
|
529
|
+
Base64.urlsafe_encode64(csrf_token, padding: false)
|
530
|
+
else
|
531
|
+
Base64.strict_encode64(csrf_token)
|
532
|
+
end
|
533
|
+
end
|
534
|
+
|
535
|
+
def decode_csrf_token(encoded_csrf_token) # :nodoc:
|
536
|
+
if urlsafe_csrf_tokens
|
537
|
+
Base64.urlsafe_decode64(encoded_csrf_token)
|
538
|
+
else
|
539
|
+
begin
|
540
|
+
Base64.strict_decode64(encoded_csrf_token)
|
541
|
+
rescue ArgumentError
|
542
|
+
Base64.urlsafe_decode64(encoded_csrf_token)
|
543
|
+
end
|
544
|
+
end
|
545
|
+
end
|
444
546
|
end
|
445
547
|
end
|