actionpack 4.2.10 → 7.2.0.rc1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of actionpack might be problematic. Click here for more details.

Files changed (202) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +86 -600
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +13 -14
  5. data/lib/abstract_controller/asset_paths.rb +5 -1
  6. data/lib/abstract_controller/base.rb +166 -136
  7. data/lib/abstract_controller/caching/fragments.rb +149 -0
  8. data/lib/abstract_controller/caching.rb +68 -0
  9. data/lib/abstract_controller/callbacks.rb +126 -57
  10. data/lib/abstract_controller/collector.rb +13 -15
  11. data/lib/abstract_controller/deprecator.rb +9 -0
  12. data/lib/abstract_controller/error.rb +8 -0
  13. data/lib/abstract_controller/helpers.rb +181 -132
  14. data/lib/abstract_controller/logger.rb +5 -1
  15. data/lib/abstract_controller/railties/routes_helpers.rb +10 -3
  16. data/lib/abstract_controller/rendering.rb +56 -56
  17. data/lib/abstract_controller/translation.rb +29 -15
  18. data/lib/abstract_controller/url_for.rb +15 -11
  19. data/lib/abstract_controller.rb +21 -5
  20. data/lib/action_controller/api/api_rendering.rb +18 -0
  21. data/lib/action_controller/api.rb +154 -0
  22. data/lib/action_controller/base.rb +219 -155
  23. data/lib/action_controller/caching.rb +28 -68
  24. data/lib/action_controller/deprecator.rb +9 -0
  25. data/lib/action_controller/form_builder.rb +55 -0
  26. data/lib/action_controller/log_subscriber.rb +35 -22
  27. data/lib/action_controller/metal/allow_browser.rb +119 -0
  28. data/lib/action_controller/metal/basic_implicit_render.rb +17 -0
  29. data/lib/action_controller/metal/conditional_get.rb +259 -122
  30. data/lib/action_controller/metal/content_security_policy.rb +86 -0
  31. data/lib/action_controller/metal/cookies.rb +9 -5
  32. data/lib/action_controller/metal/data_streaming.rb +87 -104
  33. data/lib/action_controller/metal/default_headers.rb +21 -0
  34. data/lib/action_controller/metal/etag_with_flash.rb +22 -0
  35. data/lib/action_controller/metal/etag_with_template_digest.rb +35 -26
  36. data/lib/action_controller/metal/exceptions.rb +71 -24
  37. data/lib/action_controller/metal/flash.rb +26 -19
  38. data/lib/action_controller/metal/head.rb +45 -36
  39. data/lib/action_controller/metal/helpers.rb +80 -64
  40. data/lib/action_controller/metal/http_authentication.rb +297 -244
  41. data/lib/action_controller/metal/implicit_render.rb +57 -9
  42. data/lib/action_controller/metal/instrumentation.rb +76 -64
  43. data/lib/action_controller/metal/live.rb +238 -176
  44. data/lib/action_controller/metal/logging.rb +22 -0
  45. data/lib/action_controller/metal/mime_responds.rb +177 -166
  46. data/lib/action_controller/metal/parameter_encoding.rb +84 -0
  47. data/lib/action_controller/metal/params_wrapper.rb +145 -118
  48. data/lib/action_controller/metal/permissions_policy.rb +38 -0
  49. data/lib/action_controller/metal/rate_limiting.rb +62 -0
  50. data/lib/action_controller/metal/redirecting.rb +203 -64
  51. data/lib/action_controller/metal/renderers.rb +108 -65
  52. data/lib/action_controller/metal/rendering.rb +216 -56
  53. data/lib/action_controller/metal/request_forgery_protection.rb +496 -163
  54. data/lib/action_controller/metal/rescue.rb +19 -21
  55. data/lib/action_controller/metal/streaming.rb +179 -138
  56. data/lib/action_controller/metal/strong_parameters.rb +1058 -382
  57. data/lib/action_controller/metal/testing.rb +11 -17
  58. data/lib/action_controller/metal/url_for.rb +37 -21
  59. data/lib/action_controller/metal.rb +236 -138
  60. data/lib/action_controller/railtie.rb +89 -11
  61. data/lib/action_controller/railties/helpers.rb +5 -1
  62. data/lib/action_controller/renderer.rb +161 -0
  63. data/lib/action_controller/template_assertions.rb +13 -0
  64. data/lib/action_controller/test_case.rb +425 -497
  65. data/lib/action_controller.rb +44 -22
  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 +119 -63
  69. data/lib/action_dispatch/http/content_disposition.rb +47 -0
  70. data/lib/action_dispatch/http/content_security_policy.rb +364 -0
  71. data/lib/action_dispatch/http/filter_parameters.rb +36 -34
  72. data/lib/action_dispatch/http/filter_redirect.rb +24 -12
  73. data/lib/action_dispatch/http/headers.rb +66 -31
  74. data/lib/action_dispatch/http/mime_negotiation.rb +106 -75
  75. data/lib/action_dispatch/http/mime_type.rb +196 -136
  76. data/lib/action_dispatch/http/mime_types.rb +25 -7
  77. data/lib/action_dispatch/http/parameters.rb +97 -45
  78. data/lib/action_dispatch/http/permissions_policy.rb +187 -0
  79. data/lib/action_dispatch/http/rack_cache.rb +6 -0
  80. data/lib/action_dispatch/http/request.rb +299 -170
  81. data/lib/action_dispatch/http/response.rb +311 -160
  82. data/lib/action_dispatch/http/upload.rb +52 -23
  83. data/lib/action_dispatch/http/url.rb +201 -125
  84. data/lib/action_dispatch/journey/formatter.rb +110 -50
  85. data/lib/action_dispatch/journey/gtg/builder.rb +37 -50
  86. data/lib/action_dispatch/journey/gtg/simulator.rb +20 -17
  87. data/lib/action_dispatch/journey/gtg/transition_table.rb +96 -36
  88. data/lib/action_dispatch/journey/nfa/dot.rb +5 -14
  89. data/lib/action_dispatch/journey/nodes/node.rb +100 -20
  90. data/lib/action_dispatch/journey/parser.rb +19 -17
  91. data/lib/action_dispatch/journey/parser.y +4 -3
  92. data/lib/action_dispatch/journey/parser_extras.rb +14 -4
  93. data/lib/action_dispatch/journey/path/pattern.rb +79 -63
  94. data/lib/action_dispatch/journey/route.rb +108 -44
  95. data/lib/action_dispatch/journey/router/utils.rb +41 -29
  96. data/lib/action_dispatch/journey/router.rb +64 -57
  97. data/lib/action_dispatch/journey/routes.rb +23 -21
  98. data/lib/action_dispatch/journey/scanner.rb +28 -17
  99. data/lib/action_dispatch/journey/visitors.rb +100 -54
  100. data/lib/action_dispatch/journey/visualizer/fsm.js +49 -24
  101. data/lib/action_dispatch/journey/visualizer/index.html.erb +1 -1
  102. data/lib/action_dispatch/journey.rb +7 -5
  103. data/lib/action_dispatch/log_subscriber.rb +25 -0
  104. data/lib/action_dispatch/middleware/actionable_exceptions.rb +46 -0
  105. data/lib/action_dispatch/middleware/assume_ssl.rb +27 -0
  106. data/lib/action_dispatch/middleware/callbacks.rb +7 -6
  107. data/lib/action_dispatch/middleware/cookies.rb +471 -328
  108. data/lib/action_dispatch/middleware/debug_exceptions.rb +149 -66
  109. data/lib/action_dispatch/middleware/debug_locks.rb +129 -0
  110. data/lib/action_dispatch/middleware/debug_view.rb +73 -0
  111. data/lib/action_dispatch/middleware/exception_wrapper.rb +275 -73
  112. data/lib/action_dispatch/middleware/executor.rb +32 -0
  113. data/lib/action_dispatch/middleware/flash.rb +143 -101
  114. data/lib/action_dispatch/middleware/host_authorization.rb +171 -0
  115. data/lib/action_dispatch/middleware/public_exceptions.rb +36 -27
  116. data/lib/action_dispatch/middleware/reloader.rb +10 -92
  117. data/lib/action_dispatch/middleware/remote_ip.rb +133 -107
  118. data/lib/action_dispatch/middleware/request_id.rb +29 -15
  119. data/lib/action_dispatch/middleware/server_timing.rb +78 -0
  120. data/lib/action_dispatch/middleware/session/abstract_store.rb +49 -27
  121. data/lib/action_dispatch/middleware/session/cache_store.rb +33 -16
  122. data/lib/action_dispatch/middleware/session/cookie_store.rb +86 -80
  123. data/lib/action_dispatch/middleware/session/mem_cache_store.rb +15 -3
  124. data/lib/action_dispatch/middleware/show_exceptions.rb +66 -36
  125. data/lib/action_dispatch/middleware/ssl.rb +134 -36
  126. data/lib/action_dispatch/middleware/stack.rb +109 -44
  127. data/lib/action_dispatch/middleware/static.rb +159 -90
  128. data/lib/action_dispatch/middleware/templates/rescues/_actions.html.erb +13 -0
  129. data/lib/action_dispatch/middleware/templates/rescues/_actions.text.erb +0 -0
  130. data/lib/action_dispatch/middleware/templates/rescues/_message_and_suggestions.html.erb +22 -0
  131. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb +7 -24
  132. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.text.erb +1 -1
  133. data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +36 -0
  134. data/lib/action_dispatch/middleware/templates/rescues/_source.text.erb +8 -0
  135. data/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +46 -36
  136. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb +12 -0
  137. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.text.erb +9 -0
  138. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +26 -7
  139. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb +3 -3
  140. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +24 -0
  141. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +16 -0
  142. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +139 -15
  143. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb +23 -0
  144. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.text.erb +3 -0
  145. data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +6 -6
  146. data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +7 -7
  147. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +9 -9
  148. data/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb +1 -1
  149. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +4 -4
  150. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.text.erb +1 -1
  151. data/lib/action_dispatch/middleware/templates/routes/_route.html.erb +7 -4
  152. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +125 -93
  153. data/lib/action_dispatch/railtie.rb +44 -16
  154. data/lib/action_dispatch/request/session.rb +159 -69
  155. data/lib/action_dispatch/request/utils.rb +97 -23
  156. data/lib/action_dispatch/routing/endpoint.rb +11 -2
  157. data/lib/action_dispatch/routing/inspector.rb +195 -106
  158. data/lib/action_dispatch/routing/mapper.rb +1338 -955
  159. data/lib/action_dispatch/routing/polymorphic_routes.rb +234 -201
  160. data/lib/action_dispatch/routing/redirection.rb +78 -51
  161. data/lib/action_dispatch/routing/route_set.rb +460 -374
  162. data/lib/action_dispatch/routing/routes_proxy.rb +36 -12
  163. data/lib/action_dispatch/routing/url_for.rb +172 -124
  164. data/lib/action_dispatch/routing.rb +159 -158
  165. data/lib/action_dispatch/system_test_case.rb +206 -0
  166. data/lib/action_dispatch/system_testing/browser.rb +84 -0
  167. data/lib/action_dispatch/system_testing/driver.rb +85 -0
  168. data/lib/action_dispatch/system_testing/server.rb +33 -0
  169. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +164 -0
  170. data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +23 -0
  171. data/lib/action_dispatch/testing/assertion_response.rb +48 -0
  172. data/lib/action_dispatch/testing/assertions/response.rb +71 -39
  173. data/lib/action_dispatch/testing/assertions/routing.rb +228 -103
  174. data/lib/action_dispatch/testing/assertions.rb +9 -6
  175. data/lib/action_dispatch/testing/integration.rb +486 -306
  176. data/lib/action_dispatch/testing/request_encoder.rb +60 -0
  177. data/lib/action_dispatch/testing/test_helpers/page_dump_helper.rb +35 -0
  178. data/lib/action_dispatch/testing/test_process.rb +35 -22
  179. data/lib/action_dispatch/testing/test_request.rb +29 -34
  180. data/lib/action_dispatch/testing/test_response.rb +48 -15
  181. data/lib/action_dispatch.rb +82 -40
  182. data/lib/action_pack/gem_version.rb +8 -4
  183. data/lib/action_pack/version.rb +6 -2
  184. data/lib/action_pack.rb +21 -18
  185. metadata +146 -56
  186. data/lib/action_controller/caching/fragments.rb +0 -103
  187. data/lib/action_controller/metal/force_ssl.rb +0 -97
  188. data/lib/action_controller/metal/hide_actions.rb +0 -40
  189. data/lib/action_controller/metal/rack_delegation.rb +0 -32
  190. data/lib/action_controller/middleware.rb +0 -39
  191. data/lib/action_controller/model_naming.rb +0 -12
  192. data/lib/action_dispatch/http/parameter_filter.rb +0 -72
  193. data/lib/action_dispatch/journey/backwards.rb +0 -5
  194. data/lib/action_dispatch/journey/nfa/builder.rb +0 -76
  195. data/lib/action_dispatch/journey/nfa/simulator.rb +0 -47
  196. data/lib/action_dispatch/journey/nfa/transition_table.rb +0 -163
  197. data/lib/action_dispatch/journey/router/strexp.rb +0 -27
  198. data/lib/action_dispatch/middleware/params_parser.rb +0 -60
  199. data/lib/action_dispatch/middleware/templates/rescues/_source.erb +0 -27
  200. data/lib/action_dispatch/testing/assertions/dom.rb +0 -3
  201. data/lib/action_dispatch/testing/assertions/selector.rb +0 -3
  202. data/lib/action_dispatch/testing/assertions/tag.rb +0 -3
@@ -1,63 +1,76 @@
1
- require 'rack/session/abstract/id'
2
- require 'action_controller/metal/exceptions'
3
- require 'active_support/security_utils'
1
+ # frozen_string_literal: true
4
2
 
5
- module ActionController #:nodoc:
6
- class InvalidAuthenticityToken < ActionControllerError #:nodoc:
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:
7
11
  end
8
12
 
9
- class InvalidCrossOriginRequest < ActionControllerError #:nodoc:
13
+ class InvalidCrossOriginRequest < ActionControllerError # :nodoc:
10
14
  end
11
15
 
12
- # Controller actions are protected from Cross-Site Request Forgery (CSRF) attacks
13
- # by including a token in the rendered HTML for your application. This token is
14
- # stored as a random string in the session, to which an attacker does not have
15
- # access. When a request reaches your application, \Rails verifies the received
16
- # token with the token in the session. Only HTML and JavaScript requests are checked,
17
- # so this will not protect your XML API (presumably you'll have a different
18
- # authentication scheme there anyway).
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.
19
31
  #
20
32
  # GET requests are not protected since they don't have side effects like writing
21
33
  # to the database and don't leak sensitive information. JavaScript requests are
22
- # an exception: a third-party site can use a <script> tag to reference a JavaScript
23
- # URL on your site. When your JavaScript response loads on their site, it executes.
24
- # With carefully crafted JavaScript on their end, sensitive data in your JavaScript
25
- # response may be extracted. To prevent this, only XmlHttpRequest (known as XHR or
26
- # Ajax) requests are allowed to make GET requests for JavaScript responses.
27
- #
28
- # It's important to remember that XML or JSON requests are also affected and if
29
- # you're building an API you'll need something like:
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.
30
40
  #
31
- # class ApplicationController < ActionController::Base
32
- # protect_from_forgery
33
- # skip_before_action :verify_authenticity_token, if: :json_request?
41
+ # Subclasses of ActionController::Base are protected by default with the
42
+ # `:exception` strategy, which raises an
43
+ # ActionController::InvalidAuthenticityToken error on unverified requests.
34
44
  #
35
- # protected
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:
36
49
  #
37
- # def json_request?
38
- # request.format.json?
50
+ # class ApplicationController < ActionController::Base
51
+ # protect_from_forgery with: :null_session
39
52
  # end
40
- # end
41
53
  #
42
- # CSRF protection is turned on with the <tt>protect_from_forgery</tt> method,
43
- # which checks the token and resets the session if it doesn't match what was expected.
44
- # A call to this method is generated for new \Rails applications by default.
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.
45
56
  #
46
- # The token parameter is named <tt>authenticity_token</tt> by default. The name and
47
- # value of this token must be added to every layout that renders forms by including
48
- # <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`.
49
60
  #
50
- # Learn more about CSRF attacks and securing your application in the
51
- # {Ruby on Rails Security Guide}[http://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).
52
63
  module RequestForgeryProtection
64
+ CSRF_TOKEN = "action_controller.csrf_token"
65
+
53
66
  extend ActiveSupport::Concern
54
67
 
55
68
  include AbstractController::Helpers
56
69
  include AbstractController::Callbacks
57
70
 
58
71
  included do
59
- # Sets the token parameter name for RequestForgery. Calling +protect_from_forgery+
60
- # 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.
61
74
  config_accessor :request_forgery_protection_token
62
75
  self.request_forgery_protection_token ||= :authenticity_token
63
76
 
@@ -65,7 +78,8 @@ module ActionController #:nodoc:
65
78
  config_accessor :forgery_protection_strategy
66
79
  self.forgery_protection_strategy = nil
67
80
 
68
- # 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.
69
83
  config_accessor :allow_forgery_protection
70
84
  self.allow_forgery_protection = true if allow_forgery_protection.nil?
71
85
 
@@ -73,46 +87,165 @@ module ActionController #:nodoc:
73
87
  config_accessor :log_warning_on_csrf_failure
74
88
  self.log_warning_on_csrf_failure = true
75
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
+
76
102
  helper_method :form_authenticity_token
77
103
  helper_method :protect_against_forgery?
78
104
  end
79
105
 
80
106
  module ClassMethods
81
- # 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.
82
109
  #
83
- # class ApplicationController < ActionController::Base
84
- # protect_from_forgery
85
- # end
110
+ # class ApplicationController < ActionController::Base
111
+ # protect_from_forgery
112
+ # end
86
113
  #
87
- # class FooController < ApplicationController
88
- # protect_from_forgery except: :index
114
+ # class FooController < ApplicationController
115
+ # protect_from_forgery except: :index
116
+ # end
89
117
  #
90
- # You can disable CSRF protection on controller by skipping the verification before_action:
91
- # skip_before_action :verify_authenticity_token
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
92
124
  #
93
125
  # Valid Options:
94
126
  #
95
- # * <tt>:only/:except</tt> - Passed to the <tt>before_action</tt> call. Set which actions are verified.
96
- # * <tt>:with</tt> - Set the method to handle unverified request.
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
97
164
  #
98
- # Valid unverified request handling methods are:
99
- # * <tt>:exception</tt> - Raises ActionController::InvalidAuthenticityToken exception.
100
- # * <tt>:reset_session</tt> - Resets the session.
101
- # * <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.
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
102
197
  def protect_from_forgery(options = {})
198
+ options = options.reverse_merge(prepend: false)
199
+
103
200
  self.forgery_protection_strategy = protection_method_class(options[:with] || :null_session)
104
201
  self.request_forgery_protection_token ||= :authenticity_token
105
- prepend_before_action :verify_authenticity_token, options
202
+
203
+ self.csrf_token_storage_strategy = storage_strategy(options[:store] || SessionStore.new)
204
+
205
+ before_action :verify_authenticity_token, options
106
206
  append_after_action :verify_same_origin_request
107
207
  end
108
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
+
109
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
110
233
 
111
- def protection_method_class(name)
112
- ActionController::RequestForgeryProtection::ProtectionMethods.const_get(name.to_s.classify)
113
- rescue NameError
114
- raise ArgumentError, 'Invalid request forgery protection method, use :null_session, :exception, or :reset_session'
115
- end
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
116
249
  end
117
250
 
118
251
  module ProtectionMethods
@@ -121,45 +254,41 @@ module ActionController #:nodoc:
121
254
  @controller = controller
122
255
  end
123
256
 
124
- # 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.
125
259
  def handle_unverified_request
126
260
  request = @controller.request
127
- request.session = NullSessionHash.new(request.env)
128
- request.env['action_dispatch.request.flash_hash'] = nil
129
- request.env['rack.session.options'] = { skip: true }
130
- request.env['action_dispatch.cookies'] = NullCookieJar.build(request)
261
+ request.session = NullSessionHash.new(request)
262
+ request.flash = nil
263
+ request.session_options = { skip: true }
264
+ request.cookie_jar = NullCookieJar.build(request, {})
131
265
  end
132
266
 
133
- protected
267
+ private
268
+ class NullSessionHash < Rack::Session::Abstract::SessionHash
269
+ def initialize(req)
270
+ super(nil, req)
271
+ @data = {}
272
+ @loaded = true
273
+ end
134
274
 
135
- class NullSessionHash < Rack::Session::Abstract::SessionHash #:nodoc:
136
- def initialize(env)
137
- super(nil, env)
138
- @data = {}
139
- @loaded = true
140
- end
275
+ # no-op
276
+ def destroy; end
141
277
 
142
- # no-op
143
- def destroy; end
278
+ def exists?
279
+ true
280
+ end
144
281
 
145
- def exists?
146
- true
282
+ def enabled?
283
+ false
284
+ end
147
285
  end
148
- end
149
-
150
- class NullCookieJar < ActionDispatch::Cookies::CookieJar #:nodoc:
151
- def self.build(request)
152
- key_generator = request.env[ActionDispatch::Cookies::GENERATOR_KEY]
153
- host = request.host
154
- secure = request.ssl?
155
286
 
156
- new(key_generator, host, secure, options_for_env({}))
287
+ class NullCookieJar < ActionDispatch::Cookies::CookieJar
288
+ def write(*)
289
+ # nothing
290
+ end
157
291
  end
158
-
159
- def write(*)
160
- # nothing
161
- end
162
- end
163
292
  end
164
293
 
165
294
  class ResetSession
@@ -173,162 +302,366 @@ module ActionController #:nodoc:
173
302
  end
174
303
 
175
304
  class Exception
305
+ attr_accessor :warning_message
306
+
176
307
  def initialize(controller)
177
308
  @controller = controller
178
309
  end
179
310
 
180
311
  def handle_unverified_request
181
- raise ActionController::InvalidAuthenticityToken
312
+ raise ActionController::InvalidAuthenticityToken, warning_message
182
313
  end
183
314
  end
184
315
  end
185
316
 
186
- protected
187
- # The actual before_action that is used to verify the CSRF token.
188
- # Don't override this directly. Provide your own forgery protection
189
- # strategy instead. If you override, you'll disable same-origin
190
- # `<script>` verification.
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.
191
383
  #
192
- # Lean on the protect_from_forgery declaration to mark which actions are
193
- # due for same-origin request verification. If protect_from_forgery is
194
- # enabled on an action, this before_action flags its after_action to
195
- # verify that JavaScript responses are for XHR requests, ensuring they
196
- # follow the browser's same-origin policy.
197
- def verify_authenticity_token
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:
198
390
  mark_for_same_origin_verification!
199
391
 
200
392
  if !verified_request?
201
- if logger && log_warning_on_csrf_failure
202
- logger.warn "Can't verify CSRF token authenticity"
203
- end
393
+ logger.warn unverified_request_warning_message if logger && log_warning_on_csrf_failure
394
+
204
395
  handle_unverified_request
205
396
  end
206
397
  end
207
398
 
208
399
  def handle_unverified_request
209
- forgery_protection_strategy.new(self).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
210
415
  end
211
416
 
212
- #:nodoc:
213
417
  CROSS_ORIGIN_JAVASCRIPT_WARNING = "Security warning: an embedded " \
214
418
  "<script> tag on another site requested protected JavaScript. " \
215
419
  "If you know what you're doing, go ahead and disable forgery " \
216
420
  "protection on this action to permit cross-origin JavaScript embedding."
217
421
  private_constant :CROSS_ORIGIN_JAVASCRIPT_WARNING
422
+ # :startdoc:
218
423
 
219
424
  # If `verify_authenticity_token` was run (indicating that we have
220
- # forgery protection enabled for this request) then also verify that
221
- # we aren't serving an unauthorized cross-origin response.
222
- def verify_same_origin_request
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:
223
428
  if marked_for_same_origin_verification? && non_xhr_javascript_response?
224
- logger.warn CROSS_ORIGIN_JAVASCRIPT_WARNING if logger
429
+ if logger && log_warning_on_csrf_failure
430
+ logger.warn CROSS_ORIGIN_JAVASCRIPT_WARNING
431
+ end
225
432
  raise ActionController::InvalidCrossOriginRequest, CROSS_ORIGIN_JAVASCRIPT_WARNING
226
433
  end
227
434
  end
228
435
 
229
436
  # GET requests are checked for cross-origin JavaScript after rendering.
230
- def mark_for_same_origin_verification!
231
- @marked_for_same_origin_verification = request.get?
437
+ def mark_for_same_origin_verification! # :doc:
438
+ @_marked_for_same_origin_verification = request.get?
232
439
  end
233
440
 
234
- # If the `verify_authenticity_token` before_action ran, verify that
235
- # JavaScript responses are only served to same-origin GET requests.
236
- def marked_for_same_origin_verification?
237
- @marked_for_same_origin_verification ||= false
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
238
445
  end
239
446
 
240
447
  # Check for cross-origin JavaScript responses.
241
- def non_xhr_javascript_response?
242
- content_type =~ %r(\Atext/javascript) && !request.xhr?
448
+ def non_xhr_javascript_response? # :doc:
449
+ %r(\A(?:text|application)/javascript).match?(media_type) && !request.xhr?
243
450
  end
244
451
 
245
452
  AUTHENTICITY_TOKEN_LENGTH = 32
246
453
 
247
454
  # Returns true or false if a request is verified. Checks:
248
455
  #
249
- # * is it a GET or HEAD request? Gets should be safe and idempotent
250
- # * Does the form_authenticity_token match the given token value from the params?
251
- # * Does the X-CSRF-Token header match the form_authenticity_token
252
- def verified_request?
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:
253
462
  !protect_against_forgery? || request.get? || request.head? ||
254
- valid_authenticity_token?(session, form_authenticity_param) ||
255
- valid_authenticity_token?(session, request.headers['X-CSRF-Token'])
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
256
471
  end
257
472
 
258
- # Sets the token value for the current session.
259
- def form_authenticity_token
260
- masked_authenticity_token(session)
473
+ # Possible authenticity tokens sent in the request.
474
+ def request_authenticity_tokens # :doc:
475
+ [form_authenticity_param, request.x_csrf_token]
261
476
  end
262
477
 
263
- # Creates a masked version of the authenticity token that varies
264
- # on each request. The masking is used to mitigate SSL attacks
265
- # like BREACH.
266
- def masked_authenticity_token(session)
267
- one_time_pad = SecureRandom.random_bytes(AUTHENTICITY_TOKEN_LENGTH)
268
- encrypted_csrf_token = xor_byte_strings(one_time_pad, real_csrf_token(session))
269
- masked_token = one_time_pad + encrypted_csrf_token
270
- Base64.strict_encode64(masked_token)
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)
271
481
  end
272
482
 
273
- # Checks the client's masked token to see if it matches the
274
- # session token. Essentially the inverse of
275
- # +masked_authenticity_token+.
276
- def valid_authenticity_token?(session, encoded_masked_token)
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:
277
501
  if encoded_masked_token.nil? || encoded_masked_token.empty? || !encoded_masked_token.is_a?(String)
278
502
  return false
279
503
  end
280
504
 
281
505
  begin
282
- masked_token = Base64.strict_decode64(encoded_masked_token)
506
+ masked_token = decode_csrf_token(encoded_masked_token)
283
507
  rescue ArgumentError # encoded_masked_token is invalid Base64
284
508
  return false
285
509
  end
286
510
 
287
- # See if it's actually a masked token or not. In order to
288
- # deploy this code, we should be able to handle any unmasked
289
- # 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.
290
513
 
291
514
  if masked_token.length == AUTHENTICITY_TOKEN_LENGTH
292
- # This is actually an unmasked token. This is expected if
293
- # you have just upgraded to masked tokens, but should stop
294
- # happening shortly after installing this gem
295
- 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
296
518
 
297
519
  elsif masked_token.length == AUTHENTICITY_TOKEN_LENGTH * 2
298
- # Split the token into the one-time pad and the encrypted
299
- # value and decrypt it
300
- one_time_pad = masked_token[0...AUTHENTICITY_TOKEN_LENGTH]
301
- encrypted_csrf_token = masked_token[AUTHENTICITY_TOKEN_LENGTH..-1]
302
- csrf_token = xor_byte_strings(one_time_pad, encrypted_csrf_token)
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
303
529
 
304
- compare_with_real_token csrf_token, session
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
305
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)
306
561
  else
307
- false # Token is malformed
562
+ false
308
563
  end
309
564
  end
310
565
 
311
- def compare_with_real_token(token, session)
312
- ActiveSupport::SecurityUtils.secure_compare(token, real_csrf_token(session))
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)
313
572
  end
314
573
 
315
- def real_csrf_token(session)
316
- session[:_csrf_token] ||= SecureRandom.base64(AUTHENTICITY_TOKEN_LENGTH)
317
- Base64.strict_decode64(session[:_csrf_token])
574
+ def per_form_csrf_token(session, action_path, method) # :doc:
575
+ csrf_token_hmac(session, [action_path, method.downcase].join("#"))
318
576
  end
319
577
 
320
- def xor_byte_strings(s1, s2)
321
- s1.bytes.zip(s2.bytes).map { |(c1,c2)| c1 ^ c2 }.pack('c*')
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
322
602
  end
323
603
 
324
604
  # The form's authenticity parameter. Override to provide your own.
325
- def form_authenticity_param
605
+ def form_authenticity_param # :doc:
326
606
  params[request_forgery_protection_token]
327
607
  end
328
608
 
329
609
  # Checks if the controller allows forgery protection.
330
- def protect_against_forgery?
331
- allow_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)
332
665
  end
333
666
  end
334
667
  end