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.
Files changed (171) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +94 -500
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +2 -2
  5. data/lib/abstract_controller/asset_paths.rb +2 -0
  6. data/lib/abstract_controller/base.rb +119 -106
  7. data/lib/abstract_controller/caching/fragments.rb +51 -52
  8. data/lib/abstract_controller/caching.rb +2 -0
  9. data/lib/abstract_controller/callbacks.rb +94 -67
  10. data/lib/abstract_controller/collector.rb +6 -6
  11. data/lib/abstract_controller/deprecator.rb +9 -0
  12. data/lib/abstract_controller/error.rb +2 -0
  13. data/lib/abstract_controller/helpers.rb +121 -91
  14. data/lib/abstract_controller/logger.rb +2 -0
  15. data/lib/abstract_controller/railties/routes_helpers.rb +3 -16
  16. data/lib/abstract_controller/rendering.rb +14 -13
  17. data/lib/abstract_controller/translation.rb +12 -30
  18. data/lib/abstract_controller/url_for.rb +9 -5
  19. data/lib/abstract_controller.rb +8 -0
  20. data/lib/action_controller/api/api_rendering.rb +2 -0
  21. data/lib/action_controller/api.rb +78 -73
  22. data/lib/action_controller/base.rb +199 -141
  23. data/lib/action_controller/caching.rb +16 -11
  24. data/lib/action_controller/deprecator.rb +9 -0
  25. data/lib/action_controller/form_builder.rb +21 -16
  26. data/lib/action_controller/log_subscriber.rb +19 -5
  27. data/lib/action_controller/metal/allow_browser.rb +123 -0
  28. data/lib/action_controller/metal/basic_implicit_render.rb +2 -0
  29. data/lib/action_controller/metal/conditional_get.rb +187 -174
  30. data/lib/action_controller/metal/content_security_policy.rb +26 -25
  31. data/lib/action_controller/metal/cookies.rb +4 -2
  32. data/lib/action_controller/metal/data_streaming.rb +65 -54
  33. data/lib/action_controller/metal/default_headers.rb +6 -2
  34. data/lib/action_controller/metal/etag_with_flash.rb +4 -0
  35. data/lib/action_controller/metal/etag_with_template_digest.rb +18 -14
  36. data/lib/action_controller/metal/exceptions.rb +19 -9
  37. data/lib/action_controller/metal/flash.rb +12 -10
  38. data/lib/action_controller/metal/head.rb +20 -16
  39. data/lib/action_controller/metal/helpers.rb +64 -67
  40. data/lib/action_controller/metal/http_authentication.rb +214 -200
  41. data/lib/action_controller/metal/implicit_render.rb +21 -17
  42. data/lib/action_controller/metal/instrumentation.rb +22 -12
  43. data/lib/action_controller/metal/live.rb +125 -92
  44. data/lib/action_controller/metal/logging.rb +6 -4
  45. data/lib/action_controller/metal/mime_responds.rb +151 -142
  46. data/lib/action_controller/metal/parameter_encoding.rb +34 -32
  47. data/lib/action_controller/metal/params_wrapper.rb +58 -58
  48. data/lib/action_controller/metal/permissions_policy.rb +14 -13
  49. data/lib/action_controller/metal/rate_limiting.rb +62 -0
  50. data/lib/action_controller/metal/redirecting.rb +110 -84
  51. data/lib/action_controller/metal/renderers.rb +50 -49
  52. data/lib/action_controller/metal/rendering.rb +103 -82
  53. data/lib/action_controller/metal/request_forgery_protection.rb +279 -161
  54. data/lib/action_controller/metal/rescue.rb +12 -8
  55. data/lib/action_controller/metal/streaming.rb +174 -132
  56. data/lib/action_controller/metal/strong_parameters.rb +598 -473
  57. data/lib/action_controller/metal/testing.rb +2 -0
  58. data/lib/action_controller/metal/url_for.rb +23 -14
  59. data/lib/action_controller/metal.rb +145 -61
  60. data/lib/action_controller/railtie.rb +25 -9
  61. data/lib/action_controller/railties/helpers.rb +2 -0
  62. data/lib/action_controller/renderer.rb +105 -66
  63. data/lib/action_controller/template_assertions.rb +4 -2
  64. data/lib/action_controller/test_case.rb +157 -128
  65. data/lib/action_controller.rb +17 -3
  66. data/lib/action_dispatch/constants.rb +34 -0
  67. data/lib/action_dispatch/deprecator.rb +9 -0
  68. data/lib/action_dispatch/http/cache.rb +28 -29
  69. data/lib/action_dispatch/http/content_disposition.rb +2 -0
  70. data/lib/action_dispatch/http/content_security_policy.rb +69 -49
  71. data/lib/action_dispatch/http/filter_parameters.rb +27 -12
  72. data/lib/action_dispatch/http/filter_redirect.rb +22 -1
  73. data/lib/action_dispatch/http/headers.rb +23 -21
  74. data/lib/action_dispatch/http/mime_negotiation.rb +37 -48
  75. data/lib/action_dispatch/http/mime_type.rb +60 -30
  76. data/lib/action_dispatch/http/mime_types.rb +5 -1
  77. data/lib/action_dispatch/http/parameters.rb +12 -10
  78. data/lib/action_dispatch/http/permissions_policy.rb +32 -34
  79. data/lib/action_dispatch/http/rack_cache.rb +4 -0
  80. data/lib/action_dispatch/http/request.rb +132 -79
  81. data/lib/action_dispatch/http/response.rb +136 -103
  82. data/lib/action_dispatch/http/upload.rb +19 -15
  83. data/lib/action_dispatch/http/url.rb +75 -73
  84. data/lib/action_dispatch/journey/formatter.rb +19 -6
  85. data/lib/action_dispatch/journey/gtg/builder.rb +4 -3
  86. data/lib/action_dispatch/journey/gtg/simulator.rb +2 -0
  87. data/lib/action_dispatch/journey/gtg/transition_table.rb +10 -8
  88. data/lib/action_dispatch/journey/nfa/dot.rb +2 -0
  89. data/lib/action_dispatch/journey/nodes/node.rb +6 -5
  90. data/lib/action_dispatch/journey/parser.rb +4 -3
  91. data/lib/action_dispatch/journey/parser_extras.rb +2 -0
  92. data/lib/action_dispatch/journey/path/pattern.rb +18 -15
  93. data/lib/action_dispatch/journey/route.rb +12 -9
  94. data/lib/action_dispatch/journey/router/utils.rb +16 -15
  95. data/lib/action_dispatch/journey/router.rb +13 -10
  96. data/lib/action_dispatch/journey/routes.rb +6 -4
  97. data/lib/action_dispatch/journey/scanner.rb +4 -2
  98. data/lib/action_dispatch/journey/visitors.rb +2 -0
  99. data/lib/action_dispatch/journey.rb +2 -0
  100. data/lib/action_dispatch/log_subscriber.rb +25 -0
  101. data/lib/action_dispatch/middleware/actionable_exceptions.rb +7 -6
  102. data/lib/action_dispatch/middleware/assume_ssl.rb +27 -0
  103. data/lib/action_dispatch/middleware/callbacks.rb +4 -0
  104. data/lib/action_dispatch/middleware/cookies.rb +192 -194
  105. data/lib/action_dispatch/middleware/debug_exceptions.rb +36 -27
  106. data/lib/action_dispatch/middleware/debug_locks.rb +18 -13
  107. data/lib/action_dispatch/middleware/debug_view.rb +9 -2
  108. data/lib/action_dispatch/middleware/exception_wrapper.rb +181 -27
  109. data/lib/action_dispatch/middleware/executor.rb +9 -1
  110. data/lib/action_dispatch/middleware/flash.rb +65 -46
  111. data/lib/action_dispatch/middleware/host_authorization.rb +22 -17
  112. data/lib/action_dispatch/middleware/public_exceptions.rb +12 -8
  113. data/lib/action_dispatch/middleware/reloader.rb +9 -5
  114. data/lib/action_dispatch/middleware/remote_ip.rb +88 -83
  115. data/lib/action_dispatch/middleware/request_id.rb +15 -8
  116. data/lib/action_dispatch/middleware/server_timing.rb +8 -6
  117. data/lib/action_dispatch/middleware/session/abstract_store.rb +7 -0
  118. data/lib/action_dispatch/middleware/session/cache_store.rb +14 -7
  119. data/lib/action_dispatch/middleware/session/cookie_store.rb +32 -25
  120. data/lib/action_dispatch/middleware/session/mem_cache_store.rb +9 -3
  121. data/lib/action_dispatch/middleware/show_exceptions.rb +42 -28
  122. data/lib/action_dispatch/middleware/ssl.rb +60 -45
  123. data/lib/action_dispatch/middleware/stack.rb +15 -9
  124. data/lib/action_dispatch/middleware/static.rb +40 -34
  125. data/lib/action_dispatch/middleware/templates/rescues/_actions.html.erb +2 -2
  126. data/lib/action_dispatch/middleware/templates/rescues/_message_and_suggestions.html.erb +4 -4
  127. data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +8 -1
  128. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +7 -7
  129. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb +2 -2
  130. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +17 -0
  131. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb +16 -12
  132. data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +1 -1
  133. data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +3 -3
  134. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +4 -4
  135. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +1 -1
  136. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.text.erb +1 -1
  137. data/lib/action_dispatch/middleware/templates/routes/_route.html.erb +3 -0
  138. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +47 -38
  139. data/lib/action_dispatch/railtie.rb +12 -4
  140. data/lib/action_dispatch/request/session.rb +39 -27
  141. data/lib/action_dispatch/request/utils.rb +10 -3
  142. data/lib/action_dispatch/routing/endpoint.rb +2 -0
  143. data/lib/action_dispatch/routing/inspector.rb +59 -9
  144. data/lib/action_dispatch/routing/mapper.rb +686 -639
  145. data/lib/action_dispatch/routing/polymorphic_routes.rb +70 -61
  146. data/lib/action_dispatch/routing/redirection.rb +52 -38
  147. data/lib/action_dispatch/routing/route_set.rb +106 -62
  148. data/lib/action_dispatch/routing/routes_proxy.rb +16 -19
  149. data/lib/action_dispatch/routing/url_for.rb +131 -122
  150. data/lib/action_dispatch/routing.rb +152 -150
  151. data/lib/action_dispatch/system_test_case.rb +91 -81
  152. data/lib/action_dispatch/system_testing/browser.rb +27 -19
  153. data/lib/action_dispatch/system_testing/driver.rb +16 -22
  154. data/lib/action_dispatch/system_testing/server.rb +2 -0
  155. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +53 -31
  156. data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +2 -0
  157. data/lib/action_dispatch/testing/assertion_response.rb +9 -7
  158. data/lib/action_dispatch/testing/assertions/response.rb +36 -26
  159. data/lib/action_dispatch/testing/assertions/routing.rb +203 -95
  160. data/lib/action_dispatch/testing/assertions.rb +5 -1
  161. data/lib/action_dispatch/testing/integration.rb +240 -229
  162. data/lib/action_dispatch/testing/request_encoder.rb +6 -1
  163. data/lib/action_dispatch/testing/test_helpers/page_dump_helper.rb +35 -0
  164. data/lib/action_dispatch/testing/test_process.rb +14 -9
  165. data/lib/action_dispatch/testing/test_request.rb +4 -2
  166. data/lib/action_dispatch/testing/test_response.rb +34 -19
  167. data/lib/action_dispatch.rb +52 -21
  168. data/lib/action_pack/gem_version.rb +5 -3
  169. data/lib/action_pack/version.rb +3 -1
  170. data/lib/action_pack.rb +18 -17
  171. 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
- # Controller actions are protected from Cross-Site Request Forgery (CSRF) attacks
15
- # by including a token in the rendered HTML for your application. This token is
16
- # stored as a random string in the session, to which an attacker does not have
17
- # access. When a request reaches your application, \Rails verifies the received
18
- # token with the token in the session. All requests are checked except GET requests
19
- # as these should be idempotent. Keep in mind that all session-oriented requests
20
- # are CSRF protected by default, including JavaScript and HTML requests.
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
- # use session-oriented authentication for these types of requests, by using
25
- # the <tt>protect_from_forgery</tt> method in our controllers.
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 JavaScript
30
- # URL on your site. When your JavaScript response loads on their site, it executes.
31
- # With carefully crafted JavaScript on their end, sensitive data in your JavaScript
32
- # response may be extracted. To prevent this, only XmlHttpRequest (known as XHR or
33
- # Ajax) requests are allowed to make requests for JavaScript responses.
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
- # <tt>:exception</tt> strategy, which raises an
37
- # <tt>ActionController::InvalidAuthenticityToken</tt> error on unverified requests.
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 Rails.
41
- # One way to achieve this is to use the <tt>:null_session</tt> strategy instead,
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
- # class ApplicationController < ActionController::Base
45
- # protect_from_forgery with: :null_session
46
- # end
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 middleware
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 <tt>authenticity_token</tt> by default. The name and
52
- # value of this token must be added to every layout that renders forms by including
53
- # <tt>csrf_meta_tags</tt> in the HTML +head+.
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
- # {Ruby on Rails Security Guide}[https://guides.rubyonrails.org/security.html].
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 +protect_from_forgery+
65
- # sets it to <tt>:authenticity_token</tt> by default.
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 default only in test mode.
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
- # Controls whether forgery protection is enabled by default.
90
- config_accessor :default_protect_from_forgery
91
- self.default_protect_from_forgery = false
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 are not checked.
107
+ # Turn on request forgery protection. Bear in mind that GET and HEAD requests
108
+ # are not checked.
112
109
  #
113
- # class ApplicationController < ActionController::Base
114
- # protect_from_forgery
115
- # end
110
+ # class ApplicationController < ActionController::Base
111
+ # protect_from_forgery
112
+ # end
116
113
  #
117
- # class FooController < ApplicationController
118
- # protect_from_forgery except: :index
119
- # end
114
+ # class FooController < ApplicationController
115
+ # protect_from_forgery except: :index
116
+ # end
120
117
  #
121
- # You can disable forgery protection on a controller using skip_forgery_protection:
118
+ # You can disable forgery protection on a controller using
119
+ # skip_forgery_protection:
122
120
  #
123
- # class BarController < ApplicationController
124
- # skip_forgery_protection
125
- # end
121
+ # class BarController < ApplicationController
122
+ # skip_forgery_protection
123
+ # end
126
124
  #
127
125
  # Valid Options:
128
126
  #
129
- # * <tt>:only</tt> / <tt>:except</tt> - Only apply forgery protection to a subset of actions. For example <tt>only: [ :create, :create_all ]</tt>.
130
- # * <tt>:if</tt> / <tt>:unless</tt> - Turn off the forgery protection entirely depending on the passed Proc or method reference.
131
- # * <tt>:prepend</tt> - By default, the verification of the authentication token will be added at the position of the
132
- # protect_from_forgery call in your application. This means any callbacks added before are run first. This is useful
133
- # when you want your forgery protection to depend on other callbacks, like authentication methods (Oauth vs Cookie auth).
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
- # * <tt>:exception</tt> - Raises ActionController::InvalidAuthenticityToken exception.
140
- # * <tt>:reset_session</tt> - Resets the session.
141
- # * <tt>:null_session</tt> - Provides an empty session during request but doesn't reset it completely. Used as default if <tt>:with</tt> option is not specified.
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
- # class CustomStrategy
146
- # def initialize(controller)
147
- # @controller = controller
148
- # end
178
+ # You can also implement custom strategy classes for CSRF token storage:
149
179
  #
150
- # def handle_unverified_request
151
- # # Custom behaviour for unverfied request
152
- # end
153
- # end
180
+ # class CustomStore
181
+ # def fetch(request)
182
+ # # Return the token from a custom location
183
+ # end
154
184
  #
155
- # class ApplicationController < ActionController:x:Base
156
- # protect_from_forgery with: CustomStrategy
157
- # end
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
- # skip_before_action :verify_authenticity_token
211
+ # skip_before_action :verify_authenticity_token
170
212
  #
171
- # See +skip_before_action+ for allowed options.
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 found to be unverified.
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 # :nodoc:
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 # :nodoc:
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
- # Don't override this directly. Provide your own forgery protection
261
- # strategy instead. If you override, you'll disable same-origin
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
- # due for same-origin request verification. If protect_from_forgery is
266
- # enabled on an action, this before_action flags its after_action to
267
- # verify that JavaScript responses are for XHR requests, ensuring they
268
- # follow the browser's same-origin policy.
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 # :doc:
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 # :nodoc:
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 +verify_authenticity_token+ was run (indicating that we have
306
- # forgery protection enabled for this request) then also verify that
307
- # we aren't serving an unauthorized cross-origin response.
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
- @marked_for_same_origin_verification = request.get?
438
+ @_marked_for_same_origin_verification = request.get?
320
439
  end
321
440
 
322
- # If the +verify_authenticity_token+ before_action ran, verify that
323
- # JavaScript responses are only served to same-origin GET requests.
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
- @marked_for_same_origin_verification ||= false
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
- # * Is it a GET or HEAD request? GETs should be safe and idempotent
338
- # * Does the form_authenticity_token match the given token value from the params?
339
- # * Does the +X-CSRF-Token+ header match the form_authenticity_token?
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(session, form_options: form_options)
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
- # on each request. The masking is used to mitigate SSL attacks
364
- # like BREACH.
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(session, action_path, method)
490
+ per_form_csrf_token(nil, action_path, method)
371
491
  else
372
- global_csrf_token(session)
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
- # session token. Essentially the inverse of
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
- # deploy this code, we should be able to handle any unmasked
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
- # you have just upgraded to masked tokens, but should stop
399
- # happening shortly after installing this gem.
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, session) ||
406
- compare_with_real_token(csrf_token, session) ||
407
- valid_per_form_csrf_token?(csrf_token, session)
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(session) # :doc:
451
- session[:_csrf_token] ||= generate_csrf_token
452
- decode_csrf_token(session[:_csrf_token])
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
- # Origin header.
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
- def generate_csrf_token # :nodoc:
522
- if urlsafe_csrf_tokens
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
- SecureRandom.base64(AUTHENTICITY_TOKEN_LENGTH)
641
+ uri.path.chomp("/")
526
642
  end
527
643
  end
528
644
 
529
- def encode_csrf_token(csrf_token) # :nodoc:
530
- if urlsafe_csrf_tokens
531
- Base64.urlsafe_encode64(csrf_token, padding: false)
532
- else
533
- Base64.strict_encode64(csrf_token)
534
- end
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 decode_csrf_token(encoded_csrf_token) # :nodoc:
538
- if urlsafe_csrf_tokens
539
- Base64.urlsafe_decode64(encoded_csrf_token)
540
- else
541
- begin
542
- Base64.strict_decode64(encoded_csrf_token)
543
- rescue ArgumentError
544
- Base64.urlsafe_decode64(encoded_csrf_token)
545
- end
546
- end
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