actionpack 7.0.8.1 → 7.2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +94 -500
- data/MIT-LICENSE +1 -1
- data/README.rdoc +2 -2
- data/lib/abstract_controller/asset_paths.rb +2 -0
- data/lib/abstract_controller/base.rb +119 -106
- data/lib/abstract_controller/caching/fragments.rb +51 -52
- data/lib/abstract_controller/caching.rb +2 -0
- data/lib/abstract_controller/callbacks.rb +94 -67
- data/lib/abstract_controller/collector.rb +6 -6
- data/lib/abstract_controller/deprecator.rb +9 -0
- data/lib/abstract_controller/error.rb +2 -0
- data/lib/abstract_controller/helpers.rb +121 -91
- data/lib/abstract_controller/logger.rb +2 -0
- data/lib/abstract_controller/railties/routes_helpers.rb +3 -16
- data/lib/abstract_controller/rendering.rb +14 -13
- data/lib/abstract_controller/translation.rb +12 -30
- data/lib/abstract_controller/url_for.rb +9 -5
- data/lib/abstract_controller.rb +8 -0
- data/lib/action_controller/api/api_rendering.rb +2 -0
- data/lib/action_controller/api.rb +78 -73
- data/lib/action_controller/base.rb +199 -141
- data/lib/action_controller/caching.rb +16 -11
- data/lib/action_controller/deprecator.rb +9 -0
- data/lib/action_controller/form_builder.rb +21 -16
- data/lib/action_controller/log_subscriber.rb +19 -5
- data/lib/action_controller/metal/allow_browser.rb +123 -0
- data/lib/action_controller/metal/basic_implicit_render.rb +2 -0
- data/lib/action_controller/metal/conditional_get.rb +187 -174
- data/lib/action_controller/metal/content_security_policy.rb +26 -25
- data/lib/action_controller/metal/cookies.rb +4 -2
- data/lib/action_controller/metal/data_streaming.rb +65 -54
- data/lib/action_controller/metal/default_headers.rb +6 -2
- data/lib/action_controller/metal/etag_with_flash.rb +4 -0
- data/lib/action_controller/metal/etag_with_template_digest.rb +18 -14
- data/lib/action_controller/metal/exceptions.rb +19 -9
- data/lib/action_controller/metal/flash.rb +12 -10
- data/lib/action_controller/metal/head.rb +20 -16
- data/lib/action_controller/metal/helpers.rb +64 -67
- data/lib/action_controller/metal/http_authentication.rb +214 -200
- data/lib/action_controller/metal/implicit_render.rb +21 -17
- data/lib/action_controller/metal/instrumentation.rb +22 -12
- data/lib/action_controller/metal/live.rb +125 -92
- data/lib/action_controller/metal/logging.rb +6 -4
- data/lib/action_controller/metal/mime_responds.rb +151 -142
- data/lib/action_controller/metal/parameter_encoding.rb +34 -32
- data/lib/action_controller/metal/params_wrapper.rb +58 -58
- data/lib/action_controller/metal/permissions_policy.rb +14 -13
- data/lib/action_controller/metal/rate_limiting.rb +62 -0
- data/lib/action_controller/metal/redirecting.rb +110 -84
- data/lib/action_controller/metal/renderers.rb +50 -49
- data/lib/action_controller/metal/rendering.rb +103 -82
- data/lib/action_controller/metal/request_forgery_protection.rb +279 -161
- data/lib/action_controller/metal/rescue.rb +12 -8
- data/lib/action_controller/metal/streaming.rb +174 -132
- data/lib/action_controller/metal/strong_parameters.rb +598 -473
- data/lib/action_controller/metal/testing.rb +2 -0
- data/lib/action_controller/metal/url_for.rb +23 -14
- data/lib/action_controller/metal.rb +145 -61
- data/lib/action_controller/railtie.rb +25 -9
- data/lib/action_controller/railties/helpers.rb +2 -0
- data/lib/action_controller/renderer.rb +105 -66
- data/lib/action_controller/template_assertions.rb +4 -2
- data/lib/action_controller/test_case.rb +157 -128
- data/lib/action_controller.rb +17 -3
- data/lib/action_dispatch/constants.rb +34 -0
- data/lib/action_dispatch/deprecator.rb +9 -0
- data/lib/action_dispatch/http/cache.rb +28 -29
- data/lib/action_dispatch/http/content_disposition.rb +2 -0
- data/lib/action_dispatch/http/content_security_policy.rb +69 -49
- data/lib/action_dispatch/http/filter_parameters.rb +27 -12
- data/lib/action_dispatch/http/filter_redirect.rb +22 -1
- data/lib/action_dispatch/http/headers.rb +23 -21
- data/lib/action_dispatch/http/mime_negotiation.rb +37 -48
- data/lib/action_dispatch/http/mime_type.rb +60 -30
- data/lib/action_dispatch/http/mime_types.rb +5 -1
- data/lib/action_dispatch/http/parameters.rb +12 -10
- data/lib/action_dispatch/http/permissions_policy.rb +32 -34
- data/lib/action_dispatch/http/rack_cache.rb +4 -0
- data/lib/action_dispatch/http/request.rb +132 -79
- data/lib/action_dispatch/http/response.rb +136 -103
- data/lib/action_dispatch/http/upload.rb +19 -15
- data/lib/action_dispatch/http/url.rb +75 -73
- data/lib/action_dispatch/journey/formatter.rb +19 -6
- data/lib/action_dispatch/journey/gtg/builder.rb +4 -3
- data/lib/action_dispatch/journey/gtg/simulator.rb +2 -0
- data/lib/action_dispatch/journey/gtg/transition_table.rb +10 -8
- data/lib/action_dispatch/journey/nfa/dot.rb +2 -0
- data/lib/action_dispatch/journey/nodes/node.rb +6 -5
- data/lib/action_dispatch/journey/parser.rb +4 -3
- data/lib/action_dispatch/journey/parser_extras.rb +2 -0
- data/lib/action_dispatch/journey/path/pattern.rb +18 -15
- data/lib/action_dispatch/journey/route.rb +12 -9
- data/lib/action_dispatch/journey/router/utils.rb +16 -15
- data/lib/action_dispatch/journey/router.rb +13 -10
- data/lib/action_dispatch/journey/routes.rb +6 -4
- data/lib/action_dispatch/journey/scanner.rb +4 -2
- data/lib/action_dispatch/journey/visitors.rb +2 -0
- data/lib/action_dispatch/journey.rb +2 -0
- data/lib/action_dispatch/log_subscriber.rb +25 -0
- data/lib/action_dispatch/middleware/actionable_exceptions.rb +7 -6
- data/lib/action_dispatch/middleware/assume_ssl.rb +27 -0
- data/lib/action_dispatch/middleware/callbacks.rb +4 -0
- data/lib/action_dispatch/middleware/cookies.rb +192 -194
- data/lib/action_dispatch/middleware/debug_exceptions.rb +36 -27
- data/lib/action_dispatch/middleware/debug_locks.rb +18 -13
- data/lib/action_dispatch/middleware/debug_view.rb +9 -2
- data/lib/action_dispatch/middleware/exception_wrapper.rb +181 -27
- data/lib/action_dispatch/middleware/executor.rb +9 -1
- data/lib/action_dispatch/middleware/flash.rb +65 -46
- data/lib/action_dispatch/middleware/host_authorization.rb +22 -17
- data/lib/action_dispatch/middleware/public_exceptions.rb +12 -8
- data/lib/action_dispatch/middleware/reloader.rb +9 -5
- data/lib/action_dispatch/middleware/remote_ip.rb +88 -83
- data/lib/action_dispatch/middleware/request_id.rb +15 -8
- data/lib/action_dispatch/middleware/server_timing.rb +8 -6
- data/lib/action_dispatch/middleware/session/abstract_store.rb +7 -0
- data/lib/action_dispatch/middleware/session/cache_store.rb +14 -7
- data/lib/action_dispatch/middleware/session/cookie_store.rb +32 -25
- data/lib/action_dispatch/middleware/session/mem_cache_store.rb +9 -3
- data/lib/action_dispatch/middleware/show_exceptions.rb +42 -28
- data/lib/action_dispatch/middleware/ssl.rb +60 -45
- data/lib/action_dispatch/middleware/stack.rb +15 -9
- data/lib/action_dispatch/middleware/static.rb +40 -34
- 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/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 +47 -38
- data/lib/action_dispatch/railtie.rb +12 -4
- data/lib/action_dispatch/request/session.rb +39 -27
- data/lib/action_dispatch/request/utils.rb +10 -3
- data/lib/action_dispatch/routing/endpoint.rb +2 -0
- data/lib/action_dispatch/routing/inspector.rb +59 -9
- data/lib/action_dispatch/routing/mapper.rb +686 -639
- data/lib/action_dispatch/routing/polymorphic_routes.rb +70 -61
- data/lib/action_dispatch/routing/redirection.rb +52 -38
- data/lib/action_dispatch/routing/route_set.rb +106 -62
- data/lib/action_dispatch/routing/routes_proxy.rb +16 -19
- data/lib/action_dispatch/routing/url_for.rb +131 -122
- data/lib/action_dispatch/routing.rb +152 -150
- data/lib/action_dispatch/system_test_case.rb +91 -81
- data/lib/action_dispatch/system_testing/browser.rb +27 -19
- data/lib/action_dispatch/system_testing/driver.rb +16 -22
- data/lib/action_dispatch/system_testing/server.rb +2 -0
- data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +53 -31
- data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +2 -0
- data/lib/action_dispatch/testing/assertion_response.rb +9 -7
- data/lib/action_dispatch/testing/assertions/response.rb +36 -26
- data/lib/action_dispatch/testing/assertions/routing.rb +203 -95
- data/lib/action_dispatch/testing/assertions.rb +5 -1
- data/lib/action_dispatch/testing/integration.rb +240 -229
- data/lib/action_dispatch/testing/request_encoder.rb +6 -1
- data/lib/action_dispatch/testing/test_helpers/page_dump_helper.rb +35 -0
- data/lib/action_dispatch/testing/test_process.rb +14 -9
- data/lib/action_dispatch/testing/test_request.rb +4 -2
- data/lib/action_dispatch/testing/test_response.rb +34 -19
- data/lib/action_dispatch.rb +52 -21
- data/lib/action_pack/gem_version.rb +5 -3
- data/lib/action_pack/version.rb +3 -1
- data/lib/action_pack.rb +18 -17
- metadata +91 -32
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
# :markup: markdown
|
4
|
+
|
3
5
|
require "rack/session/abstract/id"
|
4
6
|
require "action_controller/metal/exceptions"
|
5
7
|
require "active_support/security_utils"
|
@@ -11,58 +13,64 @@ module ActionController # :nodoc:
|
|
11
13
|
class InvalidCrossOriginRequest < ActionControllerError # :nodoc:
|
12
14
|
end
|
13
15
|
|
14
|
-
#
|
15
|
-
#
|
16
|
-
#
|
17
|
-
#
|
18
|
-
# token
|
19
|
-
#
|
20
|
-
#
|
16
|
+
# # Action Controller Request Forgery Protection
|
17
|
+
#
|
18
|
+
# Controller actions are protected from Cross-Site Request Forgery (CSRF)
|
19
|
+
# attacks by including a token in the rendered HTML for your application. This
|
20
|
+
# token is stored as a random string in the session, to which an attacker does
|
21
|
+
# not have access. When a request reaches your application, Rails verifies the
|
22
|
+
# received token with the token in the session. All requests are checked except
|
23
|
+
# GET requests as these should be idempotent. Keep in mind that all
|
24
|
+
# session-oriented requests are CSRF protected by default, including JavaScript
|
25
|
+
# and HTML requests.
|
21
26
|
#
|
22
27
|
# Since HTML and JavaScript requests are typically made from the browser, we
|
23
|
-
# need to ensure to verify request authenticity for the web browser. We can
|
24
|
-
#
|
25
|
-
#
|
28
|
+
# need to ensure to verify request authenticity for the web browser. We can use
|
29
|
+
# session-oriented authentication for these types of requests, by using the
|
30
|
+
# `protect_from_forgery` method in our controllers.
|
26
31
|
#
|
27
32
|
# GET requests are not protected since they don't have side effects like writing
|
28
33
|
# to the database and don't leak sensitive information. JavaScript requests are
|
29
|
-
# an exception: a third-party site can use a <script> tag to reference a
|
30
|
-
# URL on your site. When your JavaScript response loads on their
|
31
|
-
# With carefully crafted JavaScript on their end, sensitive
|
32
|
-
# response may be extracted. To prevent this, only
|
33
|
-
# Ajax) requests are allowed to make requests
|
34
|
+
# an exception: a third-party site can use a <script> tag to reference a
|
35
|
+
# JavaScript URL on your site. When your JavaScript response loads on their
|
36
|
+
# site, it executes. With carefully crafted JavaScript on their end, sensitive
|
37
|
+
# data in your JavaScript response may be extracted. To prevent this, only
|
38
|
+
# XmlHttpRequest (known as XHR or Ajax) requests are allowed to make requests
|
39
|
+
# for JavaScript responses.
|
34
40
|
#
|
35
41
|
# Subclasses of ActionController::Base are protected by default with the
|
36
|
-
#
|
37
|
-
#
|
42
|
+
# `:exception` strategy, which raises an
|
43
|
+
# ActionController::InvalidAuthenticityToken error on unverified requests.
|
38
44
|
#
|
39
45
|
# 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
|
41
|
-
# One way to achieve this is to use the
|
46
|
+
# state-less: that is, the request API client handles the session instead of
|
47
|
+
# Rails. One way to achieve this is to use the `:null_session` strategy instead,
|
42
48
|
# which allows unverified requests to be handled, but with an empty session:
|
43
49
|
#
|
44
|
-
#
|
45
|
-
#
|
46
|
-
#
|
50
|
+
# class ApplicationController < ActionController::Base
|
51
|
+
# protect_from_forgery with: :null_session
|
52
|
+
# end
|
47
53
|
#
|
48
|
-
# Note that API only applications don't include this module or a session
|
49
|
-
# by default, and so don't require CSRF protection to be configured.
|
54
|
+
# Note that API only applications don't include this module or a session
|
55
|
+
# middleware by default, and so don't require CSRF protection to be configured.
|
50
56
|
#
|
51
|
-
# The token parameter is named
|
52
|
-
# value of this token must be added to every layout that renders forms by
|
53
|
-
#
|
57
|
+
# The token parameter is named `authenticity_token` by default. The name and
|
58
|
+
# value of this token must be added to every layout that renders forms by
|
59
|
+
# including `csrf_meta_tags` in the HTML `head`.
|
54
60
|
#
|
55
|
-
# Learn more about CSRF attacks and securing your application in the
|
56
|
-
#
|
61
|
+
# Learn more about CSRF attacks and securing your application in the [Ruby on
|
62
|
+
# Rails Security Guide](https://guides.rubyonrails.org/security.html).
|
57
63
|
module RequestForgeryProtection
|
64
|
+
CSRF_TOKEN = "action_controller.csrf_token"
|
65
|
+
|
58
66
|
extend ActiveSupport::Concern
|
59
67
|
|
60
68
|
include AbstractController::Helpers
|
61
69
|
include AbstractController::Callbacks
|
62
70
|
|
63
71
|
included do
|
64
|
-
# Sets the token parameter name for RequestForgery. Calling
|
65
|
-
# sets it to
|
72
|
+
# Sets the token parameter name for RequestForgery. Calling
|
73
|
+
# `protect_from_forgery` sets it to `:authenticity_token` by default.
|
66
74
|
config_accessor :request_forgery_protection_token
|
67
75
|
self.request_forgery_protection_token ||= :authenticity_token
|
68
76
|
|
@@ -70,7 +78,8 @@ module ActionController # :nodoc:
|
|
70
78
|
config_accessor :forgery_protection_strategy
|
71
79
|
self.forgery_protection_strategy = nil
|
72
80
|
|
73
|
-
# Controls whether request forgery protection is turned on or not. Turned off by
|
81
|
+
# Controls whether request forgery protection is turned on or not. Turned off by
|
82
|
+
# default only in test mode.
|
74
83
|
config_accessor :allow_forgery_protection
|
75
84
|
self.allow_forgery_protection = true if allow_forgery_protection.nil?
|
76
85
|
|
@@ -86,89 +95,122 @@ module ActionController # :nodoc:
|
|
86
95
|
config_accessor :per_form_csrf_tokens
|
87
96
|
self.per_form_csrf_tokens = false
|
88
97
|
|
89
|
-
#
|
90
|
-
config_accessor :
|
91
|
-
self.
|
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
|
98
|
+
# The strategy to use for storing and retrieving CSRF tokens.
|
99
|
+
config_accessor :csrf_token_storage_strategy
|
100
|
+
self.csrf_token_storage_strategy = SessionStore.new
|
105
101
|
|
106
102
|
helper_method :form_authenticity_token
|
107
103
|
helper_method :protect_against_forgery?
|
108
104
|
end
|
109
105
|
|
110
106
|
module ClassMethods
|
111
|
-
# Turn on request forgery protection. Bear in mind that GET and HEAD requests
|
107
|
+
# Turn on request forgery protection. Bear in mind that GET and HEAD requests
|
108
|
+
# are not checked.
|
112
109
|
#
|
113
|
-
#
|
114
|
-
#
|
115
|
-
#
|
110
|
+
# class ApplicationController < ActionController::Base
|
111
|
+
# protect_from_forgery
|
112
|
+
# end
|
116
113
|
#
|
117
|
-
#
|
118
|
-
#
|
119
|
-
#
|
114
|
+
# class FooController < ApplicationController
|
115
|
+
# protect_from_forgery except: :index
|
116
|
+
# end
|
120
117
|
#
|
121
|
-
# You can disable forgery protection on a controller using
|
118
|
+
# You can disable forgery protection on a controller using
|
119
|
+
# skip_forgery_protection:
|
122
120
|
#
|
123
|
-
#
|
124
|
-
#
|
125
|
-
#
|
121
|
+
# class BarController < ApplicationController
|
122
|
+
# skip_forgery_protection
|
123
|
+
# end
|
126
124
|
#
|
127
125
|
# Valid Options:
|
128
126
|
#
|
129
|
-
# *
|
130
|
-
#
|
131
|
-
# *
|
132
|
-
#
|
133
|
-
#
|
127
|
+
# * `:only` / `:except` - Only apply forgery protection to a subset of
|
128
|
+
# actions. For example `only: [ :create, :create_all ]`.
|
129
|
+
# * `:if` / `:unless` - Turn off the forgery protection entirely depending on
|
130
|
+
# the passed Proc or method reference.
|
131
|
+
# * `:prepend` - By default, the verification of the authentication token will
|
132
|
+
# be added at the position of the protect_from_forgery call in your
|
133
|
+
# application. This means any callbacks added before are run first. This is
|
134
|
+
# useful when you want your forgery protection to depend on other callbacks,
|
135
|
+
# like authentication methods (Oauth vs Cookie auth).
|
136
|
+
#
|
137
|
+
# If you need to add verification to the beginning of the callback chain,
|
138
|
+
# use `prepend: true`.
|
139
|
+
# * `:with` - Set the method to handle unverified request. Note if
|
140
|
+
# `default_protect_from_forgery` is true, Rails call protect_from_forgery
|
141
|
+
# with `with :exception`.
|
134
142
|
#
|
135
|
-
# If you need to add verification to the beginning of the callback chain, use <tt>prepend: true</tt>.
|
136
|
-
# * <tt>:with</tt> - Set the method to handle unverified request.
|
137
143
|
#
|
138
144
|
# Built-in unverified request handling methods are:
|
139
|
-
# *
|
140
|
-
#
|
141
|
-
# *
|
145
|
+
# * `:exception` - Raises ActionController::InvalidAuthenticityToken
|
146
|
+
# exception.
|
147
|
+
# * `:reset_session` - Resets the session.
|
148
|
+
# * `:null_session` - Provides an empty session during request but doesn't
|
149
|
+
# reset it completely. Used as default if `:with` option is not specified.
|
150
|
+
#
|
151
|
+
#
|
152
|
+
# You can also implement custom strategy classes for unverified request
|
153
|
+
# handling:
|
154
|
+
#
|
155
|
+
# class CustomStrategy
|
156
|
+
# def initialize(controller)
|
157
|
+
# @controller = controller
|
158
|
+
# end
|
159
|
+
#
|
160
|
+
# def handle_unverified_request
|
161
|
+
# # Custom behavior for unverfied request
|
162
|
+
# end
|
163
|
+
# end
|
164
|
+
#
|
165
|
+
# class ApplicationController < ActionController::Base
|
166
|
+
# protect_from_forgery with: CustomStrategy
|
167
|
+
# end
|
168
|
+
#
|
169
|
+
# * `:store` - Set the strategy to store and retrieve CSRF tokens.
|
170
|
+
#
|
171
|
+
#
|
172
|
+
# Built-in session token strategies are:
|
173
|
+
# * `:session` - Store the CSRF token in the session. Used as default if
|
174
|
+
# `:store` option is not specified.
|
175
|
+
# * `:cookie` - Store the CSRF token in an encrypted cookie.
|
142
176
|
#
|
143
|
-
# You can also implement custom strategy classes for unverified request handling:
|
144
177
|
#
|
145
|
-
#
|
146
|
-
# def initialize(controller)
|
147
|
-
# @controller = controller
|
148
|
-
# end
|
178
|
+
# You can also implement custom strategy classes for CSRF token storage:
|
149
179
|
#
|
150
|
-
#
|
151
|
-
#
|
152
|
-
#
|
153
|
-
#
|
180
|
+
# class CustomStore
|
181
|
+
# def fetch(request)
|
182
|
+
# # Return the token from a custom location
|
183
|
+
# end
|
154
184
|
#
|
155
|
-
#
|
156
|
-
#
|
157
|
-
#
|
185
|
+
# def store(request, csrf_token)
|
186
|
+
# # Store the token in a custom location
|
187
|
+
# end
|
188
|
+
#
|
189
|
+
# def reset(request)
|
190
|
+
# # Delete the stored session token
|
191
|
+
# end
|
192
|
+
# end
|
193
|
+
#
|
194
|
+
# class ApplicationController < ActionController::Base
|
195
|
+
# protect_from_forgery store: CustomStore.new
|
196
|
+
# end
|
158
197
|
def protect_from_forgery(options = {})
|
159
198
|
options = options.reverse_merge(prepend: false)
|
160
199
|
|
161
200
|
self.forgery_protection_strategy = protection_method_class(options[:with] || :null_session)
|
162
201
|
self.request_forgery_protection_token ||= :authenticity_token
|
202
|
+
|
203
|
+
self.csrf_token_storage_strategy = storage_strategy(options[:store] || SessionStore.new)
|
204
|
+
|
163
205
|
before_action :verify_authenticity_token, options
|
164
206
|
append_after_action :verify_same_origin_request
|
165
207
|
end
|
166
208
|
|
167
209
|
# Turn off request forgery protection. This is a wrapper for:
|
168
210
|
#
|
169
|
-
#
|
211
|
+
# skip_before_action :verify_authenticity_token
|
170
212
|
#
|
171
|
-
# See
|
213
|
+
# See `skip_before_action` for allowed options.
|
172
214
|
def skip_forgery_protection(options = {})
|
173
215
|
skip_before_action :verify_authenticity_token, options.reverse_merge(raise: false)
|
174
216
|
end
|
@@ -188,6 +230,22 @@ module ActionController # :nodoc:
|
|
188
230
|
raise ArgumentError, "Invalid request forgery protection method, use :null_session, :exception, :reset_session, or a custom forgery protection class."
|
189
231
|
end
|
190
232
|
end
|
233
|
+
|
234
|
+
def storage_strategy(name)
|
235
|
+
case name
|
236
|
+
when :session
|
237
|
+
SessionStore.new
|
238
|
+
when :cookie
|
239
|
+
CookieStore.new(:csrf_token)
|
240
|
+
else
|
241
|
+
return name if is_storage_strategy?(name)
|
242
|
+
raise ArgumentError, "Invalid CSRF token storage strategy, use :session, :cookie, or a custom CSRF token storage class."
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
def is_storage_strategy?(object)
|
247
|
+
object.respond_to?(:fetch) && object.respond_to?(:store) && object.respond_to?(:reset)
|
248
|
+
end
|
191
249
|
end
|
192
250
|
|
193
251
|
module ProtectionMethods
|
@@ -196,7 +254,8 @@ module ActionController # :nodoc:
|
|
196
254
|
@controller = controller
|
197
255
|
end
|
198
256
|
|
199
|
-
# This is the method that defines the application behavior when a request is
|
257
|
+
# This is the method that defines the application behavior when a request is
|
258
|
+
# found to be unverified.
|
200
259
|
def handle_unverified_request
|
201
260
|
request = @controller.request
|
202
261
|
request.session = NullSessionHash.new(request)
|
@@ -206,7 +265,7 @@ module ActionController # :nodoc:
|
|
206
265
|
end
|
207
266
|
|
208
267
|
private
|
209
|
-
class NullSessionHash < Rack::Session::Abstract::SessionHash
|
268
|
+
class NullSessionHash < Rack::Session::Abstract::SessionHash
|
210
269
|
def initialize(req)
|
211
270
|
super(nil, req)
|
212
271
|
@data = {}
|
@@ -225,7 +284,7 @@ module ActionController # :nodoc:
|
|
225
284
|
end
|
226
285
|
end
|
227
286
|
|
228
|
-
class NullCookieJar < ActionDispatch::Cookies::CookieJar
|
287
|
+
class NullCookieJar < ActionDispatch::Cookies::CookieJar
|
229
288
|
def write(*)
|
230
289
|
# nothing
|
231
290
|
end
|
@@ -255,17 +314,78 @@ module ActionController # :nodoc:
|
|
255
314
|
end
|
256
315
|
end
|
257
316
|
|
317
|
+
class SessionStore
|
318
|
+
def fetch(request)
|
319
|
+
request.session[:_csrf_token]
|
320
|
+
end
|
321
|
+
|
322
|
+
def store(request, csrf_token)
|
323
|
+
request.session[:_csrf_token] = csrf_token
|
324
|
+
end
|
325
|
+
|
326
|
+
def reset(request)
|
327
|
+
request.session.delete(:_csrf_token)
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
331
|
+
class CookieStore
|
332
|
+
def initialize(cookie = :csrf_token)
|
333
|
+
@cookie_name = cookie
|
334
|
+
end
|
335
|
+
|
336
|
+
def fetch(request)
|
337
|
+
contents = request.cookie_jar.encrypted[@cookie_name]
|
338
|
+
return nil if contents.nil?
|
339
|
+
|
340
|
+
value = JSON.parse(contents)
|
341
|
+
return nil unless value.dig("session_id", "public_id") == request.session.id_was&.public_id
|
342
|
+
|
343
|
+
value["token"]
|
344
|
+
rescue JSON::ParserError
|
345
|
+
nil
|
346
|
+
end
|
347
|
+
|
348
|
+
def store(request, csrf_token)
|
349
|
+
request.cookie_jar.encrypted.permanent[@cookie_name] = {
|
350
|
+
value: {
|
351
|
+
token: csrf_token,
|
352
|
+
session_id: request.session.id,
|
353
|
+
}.to_json,
|
354
|
+
httponly: true,
|
355
|
+
same_site: :lax,
|
356
|
+
}
|
357
|
+
end
|
358
|
+
|
359
|
+
def reset(request)
|
360
|
+
request.cookie_jar.delete(@cookie_name)
|
361
|
+
end
|
362
|
+
end
|
363
|
+
|
364
|
+
def initialize(...)
|
365
|
+
super
|
366
|
+
@_marked_for_same_origin_verification = nil
|
367
|
+
end
|
368
|
+
|
369
|
+
def reset_csrf_token(request) # :doc:
|
370
|
+
request.env.delete(CSRF_TOKEN)
|
371
|
+
csrf_token_storage_strategy.reset(request)
|
372
|
+
end
|
373
|
+
|
374
|
+
def commit_csrf_token(request) # :doc:
|
375
|
+
csrf_token = request.env[CSRF_TOKEN]
|
376
|
+
csrf_token_storage_strategy.store(request, csrf_token) unless csrf_token.nil?
|
377
|
+
end
|
378
|
+
|
258
379
|
private
|
259
|
-
# The actual before_action that is used to verify the CSRF token.
|
260
|
-
#
|
261
|
-
#
|
262
|
-
# <tt><script></tt> verification.
|
380
|
+
# The actual before_action that is used to verify the CSRF token. Don't override
|
381
|
+
# this directly. Provide your own forgery protection strategy instead. If you
|
382
|
+
# override, you'll disable same-origin `<script>` verification.
|
263
383
|
#
|
264
|
-
# Lean on the protect_from_forgery declaration to mark which actions are
|
265
|
-
#
|
266
|
-
#
|
267
|
-
#
|
268
|
-
#
|
384
|
+
# Lean on the protect_from_forgery declaration to mark which actions are due for
|
385
|
+
# same-origin request verification. If protect_from_forgery is enabled on an
|
386
|
+
# action, this before_action flags its after_action to verify that JavaScript
|
387
|
+
# responses are for XHR requests, ensuring they follow the browser's same-origin
|
388
|
+
# policy.
|
269
389
|
def verify_authenticity_token # :doc:
|
270
390
|
mark_for_same_origin_verification!
|
271
391
|
|
@@ -276,7 +396,7 @@ module ActionController # :nodoc:
|
|
276
396
|
end
|
277
397
|
end
|
278
398
|
|
279
|
-
def handle_unverified_request
|
399
|
+
def handle_unverified_request
|
280
400
|
protection_strategy = forgery_protection_strategy.new(self)
|
281
401
|
|
282
402
|
if protection_strategy.respond_to?(:warning_message)
|
@@ -286,7 +406,7 @@ module ActionController # :nodoc:
|
|
286
406
|
protection_strategy.handle_unverified_request
|
287
407
|
end
|
288
408
|
|
289
|
-
def unverified_request_warning_message
|
409
|
+
def unverified_request_warning_message
|
290
410
|
if valid_request_origin?
|
291
411
|
"Can't verify CSRF token authenticity."
|
292
412
|
else
|
@@ -294,7 +414,6 @@ module ActionController # :nodoc:
|
|
294
414
|
end
|
295
415
|
end
|
296
416
|
|
297
|
-
# :nodoc:
|
298
417
|
CROSS_ORIGIN_JAVASCRIPT_WARNING = "Security warning: an embedded " \
|
299
418
|
"<script> tag on another site requested protected JavaScript. " \
|
300
419
|
"If you know what you're doing, go ahead and disable forgery " \
|
@@ -302,9 +421,9 @@ module ActionController # :nodoc:
|
|
302
421
|
private_constant :CROSS_ORIGIN_JAVASCRIPT_WARNING
|
303
422
|
# :startdoc:
|
304
423
|
|
305
|
-
# If
|
306
|
-
# forgery protection enabled for this request) then also verify that
|
307
|
-
#
|
424
|
+
# If `verify_authenticity_token` was run (indicating that we have
|
425
|
+
# forgery protection enabled for this request) then also verify that we aren't
|
426
|
+
# serving an unauthorized cross-origin response.
|
308
427
|
def verify_same_origin_request # :doc:
|
309
428
|
if marked_for_same_origin_verification? && non_xhr_javascript_response?
|
310
429
|
if logger && log_warning_on_csrf_failure
|
@@ -316,13 +435,13 @@ module ActionController # :nodoc:
|
|
316
435
|
|
317
436
|
# GET requests are checked for cross-origin JavaScript after rendering.
|
318
437
|
def mark_for_same_origin_verification! # :doc:
|
319
|
-
@
|
438
|
+
@_marked_for_same_origin_verification = request.get?
|
320
439
|
end
|
321
440
|
|
322
|
-
# If the
|
323
|
-
#
|
441
|
+
# If the `verify_authenticity_token` before_action ran, verify that JavaScript
|
442
|
+
# responses are only served to same-origin GET requests.
|
324
443
|
def marked_for_same_origin_verification? # :doc:
|
325
|
-
@
|
444
|
+
@_marked_for_same_origin_verification ||= false
|
326
445
|
end
|
327
446
|
|
328
447
|
# Check for cross-origin JavaScript responses.
|
@@ -334,9 +453,11 @@ module ActionController # :nodoc:
|
|
334
453
|
|
335
454
|
# Returns true or false if a request is verified. Checks:
|
336
455
|
#
|
337
|
-
# *
|
338
|
-
# *
|
339
|
-
#
|
456
|
+
# * Is it a GET or HEAD request? GETs should be safe and idempotent
|
457
|
+
# * Does the form_authenticity_token match the given token value from the
|
458
|
+
# params?
|
459
|
+
# * Does the `X-CSRF-Token` header match the form_authenticity_token?
|
460
|
+
#
|
340
461
|
def verified_request? # :doc:
|
341
462
|
!protect_against_forgery? || request.get? || request.head? ||
|
342
463
|
(valid_request_origin? && any_authenticity_token_valid?)
|
@@ -356,28 +477,26 @@ module ActionController # :nodoc:
|
|
356
477
|
|
357
478
|
# Creates the authenticity token for the current request.
|
358
479
|
def form_authenticity_token(form_options: {}) # :doc:
|
359
|
-
masked_authenticity_token(
|
480
|
+
masked_authenticity_token(form_options: form_options)
|
360
481
|
end
|
361
482
|
|
362
|
-
# Creates a masked version of the authenticity token that varies
|
363
|
-
#
|
364
|
-
|
365
|
-
def masked_authenticity_token(session, form_options: {})
|
483
|
+
# Creates a masked version of the authenticity token that varies on each
|
484
|
+
# request. The masking is used to mitigate SSL attacks like BREACH.
|
485
|
+
def masked_authenticity_token(form_options: {})
|
366
486
|
action, method = form_options.values_at(:action, :method)
|
367
487
|
|
368
488
|
raw_token = if per_form_csrf_tokens && action && method
|
369
489
|
action_path = normalize_action_path(action)
|
370
|
-
per_form_csrf_token(
|
490
|
+
per_form_csrf_token(nil, action_path, method)
|
371
491
|
else
|
372
|
-
global_csrf_token
|
492
|
+
global_csrf_token
|
373
493
|
end
|
374
494
|
|
375
495
|
mask_token(raw_token)
|
376
496
|
end
|
377
497
|
|
378
|
-
# Checks the client's masked token to see if it matches the
|
379
|
-
#
|
380
|
-
# +masked_authenticity_token+.
|
498
|
+
# Checks the client's masked token to see if it matches the session token.
|
499
|
+
# Essentially the inverse of `masked_authenticity_token`.
|
381
500
|
def valid_authenticity_token?(session, encoded_masked_token) # :doc:
|
382
501
|
if encoded_masked_token.nil? || encoded_masked_token.empty? || !encoded_masked_token.is_a?(String)
|
383
502
|
return false
|
@@ -389,30 +508,27 @@ module ActionController # :nodoc:
|
|
389
508
|
return false
|
390
509
|
end
|
391
510
|
|
392
|
-
# See if it's actually a masked token or not. In order to
|
393
|
-
#
|
394
|
-
# tokens that we've issued without error.
|
511
|
+
# See if it's actually a masked token or not. In order to deploy this code, we
|
512
|
+
# should be able to handle any unmasked tokens that we've issued without error.
|
395
513
|
|
396
514
|
if masked_token.length == AUTHENTICITY_TOKEN_LENGTH
|
397
|
-
# This is actually an unmasked token. This is expected if
|
398
|
-
#
|
399
|
-
|
400
|
-
compare_with_real_token masked_token, session
|
515
|
+
# This is actually an unmasked token. This is expected if you have just upgraded
|
516
|
+
# to masked tokens, but should stop happening shortly after installing this gem.
|
517
|
+
compare_with_real_token masked_token
|
401
518
|
|
402
519
|
elsif masked_token.length == AUTHENTICITY_TOKEN_LENGTH * 2
|
403
520
|
csrf_token = unmask_token(masked_token)
|
404
521
|
|
405
|
-
compare_with_global_token(csrf_token
|
406
|
-
compare_with_real_token(csrf_token
|
407
|
-
valid_per_form_csrf_token?(csrf_token
|
522
|
+
compare_with_global_token(csrf_token) ||
|
523
|
+
compare_with_real_token(csrf_token) ||
|
524
|
+
valid_per_form_csrf_token?(csrf_token)
|
408
525
|
else
|
409
526
|
false # Token is malformed.
|
410
527
|
end
|
411
528
|
end
|
412
529
|
|
413
530
|
def unmask_token(masked_token) # :doc:
|
414
|
-
# Split the token into the one-time pad and the encrypted
|
415
|
-
# value and decrypt it.
|
531
|
+
# Split the token into the one-time pad and the encrypted value and decrypt it.
|
416
532
|
one_time_pad = masked_token[0...AUTHENTICITY_TOKEN_LENGTH]
|
417
533
|
encrypted_csrf_token = masked_token[AUTHENTICITY_TOKEN_LENGTH..-1]
|
418
534
|
xor_byte_strings(one_time_pad, encrypted_csrf_token)
|
@@ -425,15 +541,15 @@ module ActionController # :nodoc:
|
|
425
541
|
encode_csrf_token(masked_token)
|
426
542
|
end
|
427
543
|
|
428
|
-
def compare_with_real_token(token, session) # :doc:
|
544
|
+
def compare_with_real_token(token, session = nil) # :doc:
|
429
545
|
ActiveSupport::SecurityUtils.fixed_length_secure_compare(token, real_csrf_token(session))
|
430
546
|
end
|
431
547
|
|
432
|
-
def compare_with_global_token(token, session) # :doc:
|
548
|
+
def compare_with_global_token(token, session = nil) # :doc:
|
433
549
|
ActiveSupport::SecurityUtils.fixed_length_secure_compare(token, global_csrf_token(session))
|
434
550
|
end
|
435
551
|
|
436
|
-
def valid_per_form_csrf_token?(token, session) # :doc:
|
552
|
+
def valid_per_form_csrf_token?(token, session = nil) # :doc:
|
437
553
|
if per_form_csrf_tokens
|
438
554
|
correct_token = per_form_csrf_token(
|
439
555
|
session,
|
@@ -447,9 +563,12 @@ module ActionController # :nodoc:
|
|
447
563
|
end
|
448
564
|
end
|
449
565
|
|
450
|
-
def real_csrf_token(
|
451
|
-
|
452
|
-
|
566
|
+
def real_csrf_token(_session = nil) # :doc:
|
567
|
+
csrf_token = request.env.fetch(CSRF_TOKEN) do
|
568
|
+
request.env[CSRF_TOKEN] = csrf_token_storage_strategy.fetch(request) || generate_csrf_token
|
569
|
+
end
|
570
|
+
|
571
|
+
decode_csrf_token(csrf_token)
|
453
572
|
end
|
454
573
|
|
455
574
|
def per_form_csrf_token(session, action_path, method) # :doc:
|
@@ -459,7 +578,7 @@ module ActionController # :nodoc:
|
|
459
578
|
GLOBAL_CSRF_TOKEN_IDENTIFIER = "!real_csrf_token"
|
460
579
|
private_constant :GLOBAL_CSRF_TOKEN_IDENTIFIER
|
461
580
|
|
462
|
-
def global_csrf_token(session) # :doc:
|
581
|
+
def global_csrf_token(session = nil) # :doc:
|
463
582
|
csrf_token_hmac(session, GLOBAL_CSRF_TOKEN_IDENTIFIER)
|
464
583
|
end
|
465
584
|
|
@@ -501,8 +620,8 @@ module ActionController # :nodoc:
|
|
501
620
|
Rails.application.config.action_controller.forgery_protection_origin_check setting.
|
502
621
|
MSG
|
503
622
|
|
504
|
-
# Checks if the request originated from the same origin by looking at the
|
505
|
-
#
|
623
|
+
# Checks if the request originated from the same origin by looking at the Origin
|
624
|
+
# header.
|
506
625
|
def valid_request_origin? # :doc:
|
507
626
|
if forgery_protection_origin_check
|
508
627
|
# We accept blank origin headers because some user agents don't send it.
|
@@ -515,35 +634,34 @@ module ActionController # :nodoc:
|
|
515
634
|
|
516
635
|
def normalize_action_path(action_path) # :doc:
|
517
636
|
uri = URI.parse(action_path)
|
518
|
-
uri.path.chomp("/")
|
519
|
-
end
|
520
637
|
|
521
|
-
|
522
|
-
|
523
|
-
SecureRandom.urlsafe_base64(AUTHENTICITY_TOKEN_LENGTH)
|
638
|
+
if uri.relative? && (action_path.blank? || !action_path.start_with?("/"))
|
639
|
+
normalize_relative_action_path(uri.path)
|
524
640
|
else
|
525
|
-
|
641
|
+
uri.path.chomp("/")
|
526
642
|
end
|
527
643
|
end
|
528
644
|
|
529
|
-
def
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
645
|
+
def normalize_relative_action_path(rel_action_path) # :doc:
|
646
|
+
uri = URI.parse(request.path)
|
647
|
+
# add the action path to the request.path
|
648
|
+
uri.path += "/#{rel_action_path}"
|
649
|
+
# relative path with "./path"
|
650
|
+
uri.path.gsub!("/./", "/")
|
651
|
+
|
652
|
+
uri.path.chomp("/")
|
535
653
|
end
|
536
654
|
|
537
|
-
def
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
|
546
|
-
|
655
|
+
def generate_csrf_token
|
656
|
+
SecureRandom.urlsafe_base64(AUTHENTICITY_TOKEN_LENGTH)
|
657
|
+
end
|
658
|
+
|
659
|
+
def encode_csrf_token(csrf_token)
|
660
|
+
Base64.urlsafe_encode64(csrf_token, padding: false)
|
661
|
+
end
|
662
|
+
|
663
|
+
def decode_csrf_token(encoded_csrf_token)
|
664
|
+
Base64.urlsafe_decode64(encoded_csrf_token)
|
547
665
|
end
|
548
666
|
end
|
549
667
|
end
|