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