omg-actionpack 8.0.0.alpha1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +129 -0
- data/MIT-LICENSE +21 -0
- data/README.rdoc +57 -0
- data/lib/abstract_controller/asset_paths.rb +14 -0
- data/lib/abstract_controller/base.rb +299 -0
- data/lib/abstract_controller/caching/fragments.rb +149 -0
- data/lib/abstract_controller/caching.rb +68 -0
- data/lib/abstract_controller/callbacks.rb +265 -0
- data/lib/abstract_controller/collector.rb +44 -0
- data/lib/abstract_controller/deprecator.rb +9 -0
- data/lib/abstract_controller/error.rb +8 -0
- data/lib/abstract_controller/helpers.rb +243 -0
- data/lib/abstract_controller/logger.rb +16 -0
- data/lib/abstract_controller/railties/routes_helpers.rb +25 -0
- data/lib/abstract_controller/rendering.rb +126 -0
- data/lib/abstract_controller/translation.rb +42 -0
- data/lib/abstract_controller/url_for.rb +37 -0
- data/lib/abstract_controller.rb +36 -0
- data/lib/action_controller/api/api_rendering.rb +18 -0
- data/lib/action_controller/api.rb +155 -0
- data/lib/action_controller/base.rb +332 -0
- data/lib/action_controller/caching.rb +49 -0
- data/lib/action_controller/deprecator.rb +9 -0
- data/lib/action_controller/form_builder.rb +55 -0
- data/lib/action_controller/log_subscriber.rb +96 -0
- data/lib/action_controller/metal/allow_browser.rb +123 -0
- data/lib/action_controller/metal/basic_implicit_render.rb +17 -0
- data/lib/action_controller/metal/conditional_get.rb +341 -0
- data/lib/action_controller/metal/content_security_policy.rb +86 -0
- data/lib/action_controller/metal/cookies.rb +20 -0
- data/lib/action_controller/metal/data_streaming.rb +154 -0
- data/lib/action_controller/metal/default_headers.rb +21 -0
- data/lib/action_controller/metal/etag_with_flash.rb +22 -0
- data/lib/action_controller/metal/etag_with_template_digest.rb +59 -0
- data/lib/action_controller/metal/exceptions.rb +106 -0
- data/lib/action_controller/metal/flash.rb +67 -0
- data/lib/action_controller/metal/head.rb +67 -0
- data/lib/action_controller/metal/helpers.rb +129 -0
- data/lib/action_controller/metal/http_authentication.rb +565 -0
- data/lib/action_controller/metal/implicit_render.rb +67 -0
- data/lib/action_controller/metal/instrumentation.rb +120 -0
- data/lib/action_controller/metal/live.rb +398 -0
- data/lib/action_controller/metal/logging.rb +22 -0
- data/lib/action_controller/metal/mime_responds.rb +337 -0
- data/lib/action_controller/metal/parameter_encoding.rb +84 -0
- data/lib/action_controller/metal/params_wrapper.rb +312 -0
- data/lib/action_controller/metal/permissions_policy.rb +38 -0
- data/lib/action_controller/metal/rate_limiting.rb +62 -0
- data/lib/action_controller/metal/redirecting.rb +251 -0
- data/lib/action_controller/metal/renderers.rb +181 -0
- data/lib/action_controller/metal/rendering.rb +260 -0
- data/lib/action_controller/metal/request_forgery_protection.rb +667 -0
- data/lib/action_controller/metal/rescue.rb +33 -0
- data/lib/action_controller/metal/streaming.rb +183 -0
- data/lib/action_controller/metal/strong_parameters.rb +1546 -0
- data/lib/action_controller/metal/testing.rb +25 -0
- data/lib/action_controller/metal/url_for.rb +65 -0
- data/lib/action_controller/metal.rb +339 -0
- data/lib/action_controller/railtie.rb +149 -0
- data/lib/action_controller/railties/helpers.rb +26 -0
- data/lib/action_controller/renderer.rb +161 -0
- data/lib/action_controller/template_assertions.rb +13 -0
- data/lib/action_controller/test_case.rb +691 -0
- data/lib/action_controller.rb +80 -0
- data/lib/action_dispatch/constants.rb +34 -0
- data/lib/action_dispatch/deprecator.rb +9 -0
- data/lib/action_dispatch/http/cache.rb +249 -0
- data/lib/action_dispatch/http/content_disposition.rb +47 -0
- data/lib/action_dispatch/http/content_security_policy.rb +365 -0
- data/lib/action_dispatch/http/filter_parameters.rb +80 -0
- data/lib/action_dispatch/http/filter_redirect.rb +50 -0
- data/lib/action_dispatch/http/headers.rb +134 -0
- data/lib/action_dispatch/http/mime_negotiation.rb +187 -0
- data/lib/action_dispatch/http/mime_type.rb +389 -0
- data/lib/action_dispatch/http/mime_types.rb +54 -0
- data/lib/action_dispatch/http/parameters.rb +119 -0
- data/lib/action_dispatch/http/permissions_policy.rb +189 -0
- data/lib/action_dispatch/http/rack_cache.rb +67 -0
- data/lib/action_dispatch/http/request.rb +498 -0
- data/lib/action_dispatch/http/response.rb +556 -0
- data/lib/action_dispatch/http/upload.rb +107 -0
- data/lib/action_dispatch/http/url.rb +344 -0
- data/lib/action_dispatch/journey/formatter.rb +226 -0
- data/lib/action_dispatch/journey/gtg/builder.rb +149 -0
- data/lib/action_dispatch/journey/gtg/simulator.rb +50 -0
- data/lib/action_dispatch/journey/gtg/transition_table.rb +217 -0
- data/lib/action_dispatch/journey/nfa/dot.rb +27 -0
- data/lib/action_dispatch/journey/nodes/node.rb +208 -0
- data/lib/action_dispatch/journey/parser.rb +103 -0
- data/lib/action_dispatch/journey/path/pattern.rb +209 -0
- data/lib/action_dispatch/journey/route.rb +189 -0
- data/lib/action_dispatch/journey/router/utils.rb +105 -0
- data/lib/action_dispatch/journey/router.rb +151 -0
- data/lib/action_dispatch/journey/routes.rb +82 -0
- data/lib/action_dispatch/journey/scanner.rb +70 -0
- data/lib/action_dispatch/journey/visitors.rb +267 -0
- data/lib/action_dispatch/journey/visualizer/fsm.css +30 -0
- data/lib/action_dispatch/journey/visualizer/fsm.js +159 -0
- data/lib/action_dispatch/journey/visualizer/index.html.erb +52 -0
- data/lib/action_dispatch/journey.rb +7 -0
- data/lib/action_dispatch/log_subscriber.rb +25 -0
- data/lib/action_dispatch/middleware/actionable_exceptions.rb +46 -0
- data/lib/action_dispatch/middleware/assume_ssl.rb +27 -0
- data/lib/action_dispatch/middleware/callbacks.rb +38 -0
- data/lib/action_dispatch/middleware/cookies.rb +719 -0
- data/lib/action_dispatch/middleware/debug_exceptions.rb +206 -0
- data/lib/action_dispatch/middleware/debug_locks.rb +129 -0
- data/lib/action_dispatch/middleware/debug_view.rb +73 -0
- data/lib/action_dispatch/middleware/exception_wrapper.rb +350 -0
- data/lib/action_dispatch/middleware/executor.rb +32 -0
- data/lib/action_dispatch/middleware/flash.rb +318 -0
- data/lib/action_dispatch/middleware/host_authorization.rb +171 -0
- data/lib/action_dispatch/middleware/public_exceptions.rb +64 -0
- data/lib/action_dispatch/middleware/reloader.rb +16 -0
- data/lib/action_dispatch/middleware/remote_ip.rb +199 -0
- data/lib/action_dispatch/middleware/request_id.rb +50 -0
- data/lib/action_dispatch/middleware/server_timing.rb +78 -0
- data/lib/action_dispatch/middleware/session/abstract_store.rb +112 -0
- data/lib/action_dispatch/middleware/session/cache_store.rb +66 -0
- data/lib/action_dispatch/middleware/session/cookie_store.rb +129 -0
- data/lib/action_dispatch/middleware/session/mem_cache_store.rb +34 -0
- data/lib/action_dispatch/middleware/show_exceptions.rb +88 -0
- data/lib/action_dispatch/middleware/ssl.rb +180 -0
- data/lib/action_dispatch/middleware/stack.rb +194 -0
- data/lib/action_dispatch/middleware/static.rb +192 -0
- 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 +17 -0
- data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.text.erb +23 -0
- data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +36 -0
- data/lib/action_dispatch/middleware/templates/rescues/_source.text.erb +8 -0
- data/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +62 -0
- data/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb +9 -0
- data/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb +12 -0
- data/lib/action_dispatch/middleware/templates/rescues/blocked_host.text.erb +9 -0
- data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +35 -0
- data/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb +9 -0
- data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +24 -0
- data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +16 -0
- data/lib/action_dispatch/middleware/templates/rescues/layout.erb +284 -0
- data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb +23 -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 +11 -0
- data/lib/action_dispatch/middleware/templates/rescues/missing_template.text.erb +3 -0
- data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +32 -0
- data/lib/action_dispatch/middleware/templates/rescues/routing_error.text.erb +11 -0
- data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +20 -0
- data/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb +7 -0
- data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +6 -0
- data/lib/action_dispatch/middleware/templates/rescues/unknown_action.text.erb +3 -0
- data/lib/action_dispatch/middleware/templates/routes/_route.html.erb +19 -0
- data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +232 -0
- data/lib/action_dispatch/railtie.rb +77 -0
- data/lib/action_dispatch/request/session.rb +283 -0
- data/lib/action_dispatch/request/utils.rb +109 -0
- data/lib/action_dispatch/routing/endpoint.rb +19 -0
- data/lib/action_dispatch/routing/inspector.rb +323 -0
- data/lib/action_dispatch/routing/mapper.rb +2372 -0
- data/lib/action_dispatch/routing/polymorphic_routes.rb +363 -0
- data/lib/action_dispatch/routing/redirection.rb +218 -0
- data/lib/action_dispatch/routing/route_set.rb +958 -0
- data/lib/action_dispatch/routing/routes_proxy.rb +66 -0
- data/lib/action_dispatch/routing/url_for.rb +244 -0
- data/lib/action_dispatch/routing.rb +262 -0
- data/lib/action_dispatch/system_test_case.rb +206 -0
- data/lib/action_dispatch/system_testing/browser.rb +75 -0
- data/lib/action_dispatch/system_testing/driver.rb +85 -0
- data/lib/action_dispatch/system_testing/server.rb +33 -0
- data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +164 -0
- data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +23 -0
- data/lib/action_dispatch/testing/assertion_response.rb +48 -0
- data/lib/action_dispatch/testing/assertions/response.rb +114 -0
- data/lib/action_dispatch/testing/assertions/routing.rb +343 -0
- data/lib/action_dispatch/testing/assertions.rb +25 -0
- data/lib/action_dispatch/testing/integration.rb +694 -0
- data/lib/action_dispatch/testing/request_encoder.rb +60 -0
- data/lib/action_dispatch/testing/test_helpers/page_dump_helper.rb +35 -0
- data/lib/action_dispatch/testing/test_process.rb +57 -0
- data/lib/action_dispatch/testing/test_request.rb +73 -0
- data/lib/action_dispatch/testing/test_response.rb +58 -0
- data/lib/action_dispatch.rb +147 -0
- data/lib/action_pack/gem_version.rb +19 -0
- data/lib/action_pack/version.rb +12 -0
- data/lib/action_pack.rb +27 -0
- metadata +375 -0
@@ -0,0 +1,667 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# :markup: markdown
|
4
|
+
|
5
|
+
require "rack/session/abstract/id"
|
6
|
+
require "action_controller/metal/exceptions"
|
7
|
+
require "active_support/security_utils"
|
8
|
+
|
9
|
+
module ActionController # :nodoc:
|
10
|
+
class InvalidAuthenticityToken < ActionControllerError # :nodoc:
|
11
|
+
end
|
12
|
+
|
13
|
+
class InvalidCrossOriginRequest < ActionControllerError # :nodoc:
|
14
|
+
end
|
15
|
+
|
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.
|
26
|
+
#
|
27
|
+
# Since HTML and JavaScript requests are typically made from the browser, we
|
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.
|
31
|
+
#
|
32
|
+
# GET requests are not protected since they don't have side effects like writing
|
33
|
+
# to the database and don't leak sensitive information. JavaScript requests are
|
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.
|
40
|
+
#
|
41
|
+
# Subclasses of ActionController::Base are protected by default with the
|
42
|
+
# `:exception` strategy, which raises an
|
43
|
+
# ActionController::InvalidAuthenticityToken error on unverified requests.
|
44
|
+
#
|
45
|
+
# APIs may want to disable this behavior since they are typically designed to be
|
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,
|
48
|
+
# which allows unverified requests to be handled, but with an empty session:
|
49
|
+
#
|
50
|
+
# class ApplicationController < ActionController::Base
|
51
|
+
# protect_from_forgery with: :null_session
|
52
|
+
# end
|
53
|
+
#
|
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.
|
56
|
+
#
|
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`.
|
60
|
+
#
|
61
|
+
# Learn more about CSRF attacks and securing your application in the [Ruby on
|
62
|
+
# Rails Security Guide](https://guides.rubyonrails.org/security.html).
|
63
|
+
module RequestForgeryProtection
|
64
|
+
CSRF_TOKEN = "action_controller.csrf_token"
|
65
|
+
|
66
|
+
extend ActiveSupport::Concern
|
67
|
+
|
68
|
+
include AbstractController::Helpers
|
69
|
+
include AbstractController::Callbacks
|
70
|
+
|
71
|
+
included do
|
72
|
+
# Sets the token parameter name for RequestForgery. Calling
|
73
|
+
# `protect_from_forgery` sets it to `:authenticity_token` by default.
|
74
|
+
config_accessor :request_forgery_protection_token
|
75
|
+
self.request_forgery_protection_token ||= :authenticity_token
|
76
|
+
|
77
|
+
# Holds the class which implements the request forgery protection.
|
78
|
+
config_accessor :forgery_protection_strategy
|
79
|
+
self.forgery_protection_strategy = nil
|
80
|
+
|
81
|
+
# Controls whether request forgery protection is turned on or not. Turned off by
|
82
|
+
# default only in test mode.
|
83
|
+
config_accessor :allow_forgery_protection
|
84
|
+
self.allow_forgery_protection = true if allow_forgery_protection.nil?
|
85
|
+
|
86
|
+
# Controls whether a CSRF failure logs a warning. On by default.
|
87
|
+
config_accessor :log_warning_on_csrf_failure
|
88
|
+
self.log_warning_on_csrf_failure = true
|
89
|
+
|
90
|
+
# Controls whether the Origin header is checked in addition to the CSRF token.
|
91
|
+
config_accessor :forgery_protection_origin_check
|
92
|
+
self.forgery_protection_origin_check = false
|
93
|
+
|
94
|
+
# Controls whether form-action/method specific CSRF tokens are used.
|
95
|
+
config_accessor :per_form_csrf_tokens
|
96
|
+
self.per_form_csrf_tokens = false
|
97
|
+
|
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
|
101
|
+
|
102
|
+
helper_method :form_authenticity_token
|
103
|
+
helper_method :protect_against_forgery?
|
104
|
+
end
|
105
|
+
|
106
|
+
module ClassMethods
|
107
|
+
# Turn on request forgery protection. Bear in mind that GET and HEAD requests
|
108
|
+
# are not checked.
|
109
|
+
#
|
110
|
+
# class ApplicationController < ActionController::Base
|
111
|
+
# protect_from_forgery
|
112
|
+
# end
|
113
|
+
#
|
114
|
+
# class FooController < ApplicationController
|
115
|
+
# protect_from_forgery except: :index
|
116
|
+
# end
|
117
|
+
#
|
118
|
+
# You can disable forgery protection on a controller using
|
119
|
+
# skip_forgery_protection:
|
120
|
+
#
|
121
|
+
# class BarController < ApplicationController
|
122
|
+
# skip_forgery_protection
|
123
|
+
# end
|
124
|
+
#
|
125
|
+
# Valid Options:
|
126
|
+
#
|
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`.
|
142
|
+
#
|
143
|
+
#
|
144
|
+
# Built-in unverified request handling methods are:
|
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.
|
176
|
+
#
|
177
|
+
#
|
178
|
+
# You can also implement custom strategy classes for CSRF token storage:
|
179
|
+
#
|
180
|
+
# class CustomStore
|
181
|
+
# def fetch(request)
|
182
|
+
# # Return the token from a custom location
|
183
|
+
# end
|
184
|
+
#
|
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
|
197
|
+
def protect_from_forgery(options = {})
|
198
|
+
options = options.reverse_merge(prepend: false)
|
199
|
+
|
200
|
+
self.forgery_protection_strategy = protection_method_class(options[:with] || :null_session)
|
201
|
+
self.request_forgery_protection_token ||= :authenticity_token
|
202
|
+
|
203
|
+
self.csrf_token_storage_strategy = storage_strategy(options[:store] || SessionStore.new)
|
204
|
+
|
205
|
+
before_action :verify_authenticity_token, options
|
206
|
+
append_after_action :verify_same_origin_request
|
207
|
+
end
|
208
|
+
|
209
|
+
# Turn off request forgery protection. This is a wrapper for:
|
210
|
+
#
|
211
|
+
# skip_before_action :verify_authenticity_token
|
212
|
+
#
|
213
|
+
# See `skip_before_action` for allowed options.
|
214
|
+
def skip_forgery_protection(options = {})
|
215
|
+
skip_before_action :verify_authenticity_token, options.reverse_merge(raise: false)
|
216
|
+
end
|
217
|
+
|
218
|
+
private
|
219
|
+
def protection_method_class(name)
|
220
|
+
case name
|
221
|
+
when :null_session
|
222
|
+
ProtectionMethods::NullSession
|
223
|
+
when :reset_session
|
224
|
+
ProtectionMethods::ResetSession
|
225
|
+
when :exception
|
226
|
+
ProtectionMethods::Exception
|
227
|
+
when Class
|
228
|
+
name
|
229
|
+
else
|
230
|
+
raise ArgumentError, "Invalid request forgery protection method, use :null_session, :exception, :reset_session, or a custom forgery protection class."
|
231
|
+
end
|
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
|
249
|
+
end
|
250
|
+
|
251
|
+
module ProtectionMethods
|
252
|
+
class NullSession
|
253
|
+
def initialize(controller)
|
254
|
+
@controller = controller
|
255
|
+
end
|
256
|
+
|
257
|
+
# This is the method that defines the application behavior when a request is
|
258
|
+
# found to be unverified.
|
259
|
+
def handle_unverified_request
|
260
|
+
request = @controller.request
|
261
|
+
request.session = NullSessionHash.new(request)
|
262
|
+
request.flash = nil
|
263
|
+
request.session_options = { skip: true }
|
264
|
+
request.cookie_jar = NullCookieJar.build(request, {})
|
265
|
+
end
|
266
|
+
|
267
|
+
private
|
268
|
+
class NullSessionHash < Rack::Session::Abstract::SessionHash
|
269
|
+
def initialize(req)
|
270
|
+
super(nil, req)
|
271
|
+
@data = {}
|
272
|
+
@loaded = true
|
273
|
+
end
|
274
|
+
|
275
|
+
# no-op
|
276
|
+
def destroy; end
|
277
|
+
|
278
|
+
def exists?
|
279
|
+
true
|
280
|
+
end
|
281
|
+
|
282
|
+
def enabled?
|
283
|
+
false
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
class NullCookieJar < ActionDispatch::Cookies::CookieJar
|
288
|
+
def write(*)
|
289
|
+
# nothing
|
290
|
+
end
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
class ResetSession
|
295
|
+
def initialize(controller)
|
296
|
+
@controller = controller
|
297
|
+
end
|
298
|
+
|
299
|
+
def handle_unverified_request
|
300
|
+
@controller.reset_session
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
class Exception
|
305
|
+
attr_accessor :warning_message
|
306
|
+
|
307
|
+
def initialize(controller)
|
308
|
+
@controller = controller
|
309
|
+
end
|
310
|
+
|
311
|
+
def handle_unverified_request
|
312
|
+
raise ActionController::InvalidAuthenticityToken, warning_message
|
313
|
+
end
|
314
|
+
end
|
315
|
+
end
|
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
|
+
|
379
|
+
private
|
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.
|
383
|
+
#
|
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.
|
389
|
+
def verify_authenticity_token # :doc:
|
390
|
+
mark_for_same_origin_verification!
|
391
|
+
|
392
|
+
if !verified_request?
|
393
|
+
logger.warn unverified_request_warning_message if logger && log_warning_on_csrf_failure
|
394
|
+
|
395
|
+
handle_unverified_request
|
396
|
+
end
|
397
|
+
end
|
398
|
+
|
399
|
+
def handle_unverified_request
|
400
|
+
protection_strategy = forgery_protection_strategy.new(self)
|
401
|
+
|
402
|
+
if protection_strategy.respond_to?(:warning_message)
|
403
|
+
protection_strategy.warning_message = unverified_request_warning_message
|
404
|
+
end
|
405
|
+
|
406
|
+
protection_strategy.handle_unverified_request
|
407
|
+
end
|
408
|
+
|
409
|
+
def unverified_request_warning_message
|
410
|
+
if valid_request_origin?
|
411
|
+
"Can't verify CSRF token authenticity."
|
412
|
+
else
|
413
|
+
"HTTP Origin header (#{request.origin}) didn't match request.base_url (#{request.base_url})"
|
414
|
+
end
|
415
|
+
end
|
416
|
+
|
417
|
+
CROSS_ORIGIN_JAVASCRIPT_WARNING = "Security warning: an embedded " \
|
418
|
+
"<script> tag on another site requested protected JavaScript. " \
|
419
|
+
"If you know what you're doing, go ahead and disable forgery " \
|
420
|
+
"protection on this action to permit cross-origin JavaScript embedding."
|
421
|
+
private_constant :CROSS_ORIGIN_JAVASCRIPT_WARNING
|
422
|
+
# :startdoc:
|
423
|
+
|
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.
|
427
|
+
def verify_same_origin_request # :doc:
|
428
|
+
if marked_for_same_origin_verification? && non_xhr_javascript_response?
|
429
|
+
if logger && log_warning_on_csrf_failure
|
430
|
+
logger.warn CROSS_ORIGIN_JAVASCRIPT_WARNING
|
431
|
+
end
|
432
|
+
raise ActionController::InvalidCrossOriginRequest, CROSS_ORIGIN_JAVASCRIPT_WARNING
|
433
|
+
end
|
434
|
+
end
|
435
|
+
|
436
|
+
# GET requests are checked for cross-origin JavaScript after rendering.
|
437
|
+
def mark_for_same_origin_verification! # :doc:
|
438
|
+
@_marked_for_same_origin_verification = request.get?
|
439
|
+
end
|
440
|
+
|
441
|
+
# If the `verify_authenticity_token` before_action ran, verify that JavaScript
|
442
|
+
# responses are only served to same-origin GET requests.
|
443
|
+
def marked_for_same_origin_verification? # :doc:
|
444
|
+
@_marked_for_same_origin_verification ||= false
|
445
|
+
end
|
446
|
+
|
447
|
+
# Check for cross-origin JavaScript responses.
|
448
|
+
def non_xhr_javascript_response? # :doc:
|
449
|
+
%r(\A(?:text|application)/javascript).match?(media_type) && !request.xhr?
|
450
|
+
end
|
451
|
+
|
452
|
+
AUTHENTICITY_TOKEN_LENGTH = 32
|
453
|
+
|
454
|
+
# Returns true or false if a request is verified. Checks:
|
455
|
+
#
|
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
|
+
#
|
461
|
+
def verified_request? # :doc:
|
462
|
+
!protect_against_forgery? || request.get? || request.head? ||
|
463
|
+
(valid_request_origin? && any_authenticity_token_valid?)
|
464
|
+
end
|
465
|
+
|
466
|
+
# Checks if any of the authenticity tokens from the request are valid.
|
467
|
+
def any_authenticity_token_valid? # :doc:
|
468
|
+
request_authenticity_tokens.any? do |token|
|
469
|
+
valid_authenticity_token?(session, token)
|
470
|
+
end
|
471
|
+
end
|
472
|
+
|
473
|
+
# Possible authenticity tokens sent in the request.
|
474
|
+
def request_authenticity_tokens # :doc:
|
475
|
+
[form_authenticity_param, request.x_csrf_token]
|
476
|
+
end
|
477
|
+
|
478
|
+
# Creates the authenticity token for the current request.
|
479
|
+
def form_authenticity_token(form_options: {}) # :doc:
|
480
|
+
masked_authenticity_token(form_options: form_options)
|
481
|
+
end
|
482
|
+
|
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: {})
|
486
|
+
action, method = form_options.values_at(:action, :method)
|
487
|
+
|
488
|
+
raw_token = if per_form_csrf_tokens && action && method
|
489
|
+
action_path = normalize_action_path(action)
|
490
|
+
per_form_csrf_token(nil, action_path, method)
|
491
|
+
else
|
492
|
+
global_csrf_token
|
493
|
+
end
|
494
|
+
|
495
|
+
mask_token(raw_token)
|
496
|
+
end
|
497
|
+
|
498
|
+
# Checks the client's masked token to see if it matches the session token.
|
499
|
+
# Essentially the inverse of `masked_authenticity_token`.
|
500
|
+
def valid_authenticity_token?(session, encoded_masked_token) # :doc:
|
501
|
+
if encoded_masked_token.nil? || encoded_masked_token.empty? || !encoded_masked_token.is_a?(String)
|
502
|
+
return false
|
503
|
+
end
|
504
|
+
|
505
|
+
begin
|
506
|
+
masked_token = decode_csrf_token(encoded_masked_token)
|
507
|
+
rescue ArgumentError # encoded_masked_token is invalid Base64
|
508
|
+
return false
|
509
|
+
end
|
510
|
+
|
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.
|
513
|
+
|
514
|
+
if masked_token.length == AUTHENTICITY_TOKEN_LENGTH
|
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
|
518
|
+
|
519
|
+
elsif masked_token.length == AUTHENTICITY_TOKEN_LENGTH * 2
|
520
|
+
csrf_token = unmask_token(masked_token)
|
521
|
+
|
522
|
+
compare_with_global_token(csrf_token) ||
|
523
|
+
compare_with_real_token(csrf_token) ||
|
524
|
+
valid_per_form_csrf_token?(csrf_token)
|
525
|
+
else
|
526
|
+
false # Token is malformed.
|
527
|
+
end
|
528
|
+
end
|
529
|
+
|
530
|
+
def unmask_token(masked_token) # :doc:
|
531
|
+
# Split the token into the one-time pad and the encrypted value and decrypt it.
|
532
|
+
one_time_pad = masked_token[0...AUTHENTICITY_TOKEN_LENGTH]
|
533
|
+
encrypted_csrf_token = masked_token[AUTHENTICITY_TOKEN_LENGTH..-1]
|
534
|
+
xor_byte_strings(one_time_pad, encrypted_csrf_token)
|
535
|
+
end
|
536
|
+
|
537
|
+
def mask_token(raw_token) # :doc:
|
538
|
+
one_time_pad = SecureRandom.random_bytes(AUTHENTICITY_TOKEN_LENGTH)
|
539
|
+
encrypted_csrf_token = xor_byte_strings(one_time_pad, raw_token)
|
540
|
+
masked_token = one_time_pad + encrypted_csrf_token
|
541
|
+
encode_csrf_token(masked_token)
|
542
|
+
end
|
543
|
+
|
544
|
+
def compare_with_real_token(token, session = nil) # :doc:
|
545
|
+
ActiveSupport::SecurityUtils.fixed_length_secure_compare(token, real_csrf_token(session))
|
546
|
+
end
|
547
|
+
|
548
|
+
def compare_with_global_token(token, session = nil) # :doc:
|
549
|
+
ActiveSupport::SecurityUtils.fixed_length_secure_compare(token, global_csrf_token(session))
|
550
|
+
end
|
551
|
+
|
552
|
+
def valid_per_form_csrf_token?(token, session = nil) # :doc:
|
553
|
+
if per_form_csrf_tokens
|
554
|
+
correct_token = per_form_csrf_token(
|
555
|
+
session,
|
556
|
+
request.path.chomp("/"),
|
557
|
+
request.request_method
|
558
|
+
)
|
559
|
+
|
560
|
+
ActiveSupport::SecurityUtils.fixed_length_secure_compare(token, correct_token)
|
561
|
+
else
|
562
|
+
false
|
563
|
+
end
|
564
|
+
end
|
565
|
+
|
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)
|
572
|
+
end
|
573
|
+
|
574
|
+
def per_form_csrf_token(session, action_path, method) # :doc:
|
575
|
+
csrf_token_hmac(session, [action_path, method.downcase].join("#"))
|
576
|
+
end
|
577
|
+
|
578
|
+
GLOBAL_CSRF_TOKEN_IDENTIFIER = "!real_csrf_token"
|
579
|
+
private_constant :GLOBAL_CSRF_TOKEN_IDENTIFIER
|
580
|
+
|
581
|
+
def global_csrf_token(session = nil) # :doc:
|
582
|
+
csrf_token_hmac(session, GLOBAL_CSRF_TOKEN_IDENTIFIER)
|
583
|
+
end
|
584
|
+
|
585
|
+
def csrf_token_hmac(session, identifier) # :doc:
|
586
|
+
OpenSSL::HMAC.digest(
|
587
|
+
OpenSSL::Digest::SHA256.new,
|
588
|
+
real_csrf_token(session),
|
589
|
+
identifier
|
590
|
+
)
|
591
|
+
end
|
592
|
+
|
593
|
+
def xor_byte_strings(s1, s2) # :doc:
|
594
|
+
s2 = s2.dup
|
595
|
+
size = s1.bytesize
|
596
|
+
i = 0
|
597
|
+
while i < size
|
598
|
+
s2.setbyte(i, s1.getbyte(i) ^ s2.getbyte(i))
|
599
|
+
i += 1
|
600
|
+
end
|
601
|
+
s2
|
602
|
+
end
|
603
|
+
|
604
|
+
# The form's authenticity parameter. Override to provide your own.
|
605
|
+
def form_authenticity_param # :doc:
|
606
|
+
params[request_forgery_protection_token]
|
607
|
+
end
|
608
|
+
|
609
|
+
# Checks if the controller allows forgery protection.
|
610
|
+
def protect_against_forgery? # :doc:
|
611
|
+
allow_forgery_protection && (!session.respond_to?(:enabled?) || session.enabled?)
|
612
|
+
end
|
613
|
+
|
614
|
+
NULL_ORIGIN_MESSAGE = <<~MSG
|
615
|
+
The browser returned a 'null' origin for a request with origin-based forgery protection turned on. This usually
|
616
|
+
means you have the 'no-referrer' Referrer-Policy header enabled, or that the request came from a site that
|
617
|
+
refused to give its origin. This makes it impossible for Rails to verify the source of the requests. Likely the
|
618
|
+
best solution is to change your referrer policy to something less strict like same-origin or strict-origin.
|
619
|
+
If you cannot change the referrer policy, you can disable origin checking with the
|
620
|
+
Rails.application.config.action_controller.forgery_protection_origin_check setting.
|
621
|
+
MSG
|
622
|
+
|
623
|
+
# Checks if the request originated from the same origin by looking at the Origin
|
624
|
+
# header.
|
625
|
+
def valid_request_origin? # :doc:
|
626
|
+
if forgery_protection_origin_check
|
627
|
+
# We accept blank origin headers because some user agents don't send it.
|
628
|
+
raise InvalidAuthenticityToken, NULL_ORIGIN_MESSAGE if request.origin == "null"
|
629
|
+
request.origin.nil? || request.origin == request.base_url
|
630
|
+
else
|
631
|
+
true
|
632
|
+
end
|
633
|
+
end
|
634
|
+
|
635
|
+
def normalize_action_path(action_path) # :doc:
|
636
|
+
uri = URI.parse(action_path)
|
637
|
+
|
638
|
+
if uri.relative? && (action_path.blank? || !action_path.start_with?("/"))
|
639
|
+
normalize_relative_action_path(uri.path)
|
640
|
+
else
|
641
|
+
uri.path.chomp("/")
|
642
|
+
end
|
643
|
+
end
|
644
|
+
|
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("/")
|
653
|
+
end
|
654
|
+
|
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)
|
665
|
+
end
|
666
|
+
end
|
667
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# :markup: markdown
|
4
|
+
|
5
|
+
module ActionController # :nodoc:
|
6
|
+
# # Action Controller Rescue
|
7
|
+
#
|
8
|
+
# This module is responsible for providing
|
9
|
+
# [rescue_from](rdoc-ref:ActiveSupport::Rescuable::ClassMethods#rescue_from) to
|
10
|
+
# controllers, wrapping actions to handle configured errors, and configuring
|
11
|
+
# when detailed exceptions must be shown.
|
12
|
+
module Rescue
|
13
|
+
extend ActiveSupport::Concern
|
14
|
+
include ActiveSupport::Rescuable
|
15
|
+
|
16
|
+
# Override this method if you want to customize when detailed exceptions must be
|
17
|
+
# shown. This method is only called when `consider_all_requests_local` is
|
18
|
+
# `false`. By default, it returns `false`, but someone may set it to
|
19
|
+
# `request.local?` so local requests in production still show the detailed
|
20
|
+
# exception pages.
|
21
|
+
def show_detailed_exceptions?
|
22
|
+
false
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
def process_action(*)
|
27
|
+
super
|
28
|
+
rescue Exception => exception
|
29
|
+
request.env["action_dispatch.show_detailed_exceptions"] ||= show_detailed_exceptions?
|
30
|
+
rescue_with_handler(exception) || raise
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|