actionpack 4.2.11.1 → 5.2.6

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 (166) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +328 -458
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +6 -7
  5. data/lib/abstract_controller/asset_paths.rb +2 -0
  6. data/lib/abstract_controller/base.rb +45 -49
  7. data/lib/{action_controller → abstract_controller}/caching/fragments.rb +78 -15
  8. data/lib/abstract_controller/caching.rb +66 -0
  9. data/lib/abstract_controller/callbacks.rb +47 -31
  10. data/lib/abstract_controller/collector.rb +8 -11
  11. data/lib/abstract_controller/error.rb +6 -0
  12. data/lib/abstract_controller/helpers.rb +25 -25
  13. data/lib/abstract_controller/logger.rb +2 -0
  14. data/lib/abstract_controller/railties/routes_helpers.rb +4 -2
  15. data/lib/abstract_controller/rendering.rb +42 -41
  16. data/lib/abstract_controller/translation.rb +10 -7
  17. data/lib/abstract_controller/url_for.rb +2 -0
  18. data/lib/abstract_controller.rb +12 -5
  19. data/lib/action_controller/api/api_rendering.rb +16 -0
  20. data/lib/action_controller/api.rb +149 -0
  21. data/lib/action_controller/base.rb +27 -19
  22. data/lib/action_controller/caching.rb +14 -57
  23. data/lib/action_controller/form_builder.rb +50 -0
  24. data/lib/action_controller/log_subscriber.rb +10 -15
  25. data/lib/action_controller/metal/basic_implicit_render.rb +13 -0
  26. data/lib/action_controller/metal/conditional_get.rb +118 -44
  27. data/lib/action_controller/metal/content_security_policy.rb +52 -0
  28. data/lib/action_controller/metal/cookies.rb +3 -3
  29. data/lib/action_controller/metal/data_streaming.rb +27 -46
  30. data/lib/action_controller/metal/etag_with_flash.rb +18 -0
  31. data/lib/action_controller/metal/etag_with_template_digest.rb +20 -13
  32. data/lib/action_controller/metal/exceptions.rb +8 -14
  33. data/lib/action_controller/metal/flash.rb +4 -3
  34. data/lib/action_controller/metal/force_ssl.rb +23 -21
  35. data/lib/action_controller/metal/head.rb +21 -19
  36. data/lib/action_controller/metal/helpers.rb +24 -14
  37. data/lib/action_controller/metal/http_authentication.rb +65 -58
  38. data/lib/action_controller/metal/implicit_render.rb +62 -8
  39. data/lib/action_controller/metal/instrumentation.rb +19 -21
  40. data/lib/action_controller/metal/live.rb +90 -106
  41. data/lib/action_controller/metal/mime_responds.rb +33 -46
  42. data/lib/action_controller/metal/parameter_encoding.rb +51 -0
  43. data/lib/action_controller/metal/params_wrapper.rb +61 -53
  44. data/lib/action_controller/metal/redirecting.rb +49 -28
  45. data/lib/action_controller/metal/renderers.rb +87 -44
  46. data/lib/action_controller/metal/rendering.rb +72 -50
  47. data/lib/action_controller/metal/request_forgery_protection.rb +284 -97
  48. data/lib/action_controller/metal/rescue.rb +9 -16
  49. data/lib/action_controller/metal/streaming.rb +12 -10
  50. data/lib/action_controller/metal/strong_parameters.rb +583 -164
  51. data/lib/action_controller/metal/testing.rb +2 -17
  52. data/lib/action_controller/metal/url_for.rb +19 -10
  53. data/lib/action_controller/metal.rb +98 -83
  54. data/lib/action_controller/railtie.rb +28 -10
  55. data/lib/action_controller/railties/helpers.rb +2 -0
  56. data/lib/action_controller/renderer.rb +117 -0
  57. data/lib/action_controller/template_assertions.rb +11 -0
  58. data/lib/action_controller/test_case.rb +282 -413
  59. data/lib/action_controller.rb +29 -21
  60. data/lib/action_dispatch/http/cache.rb +93 -47
  61. data/lib/action_dispatch/http/content_security_policy.rb +272 -0
  62. data/lib/action_dispatch/http/filter_parameters.rb +26 -20
  63. data/lib/action_dispatch/http/filter_redirect.rb +10 -11
  64. data/lib/action_dispatch/http/headers.rb +55 -22
  65. data/lib/action_dispatch/http/mime_negotiation.rb +56 -41
  66. data/lib/action_dispatch/http/mime_type.rb +134 -121
  67. data/lib/action_dispatch/http/mime_types.rb +20 -6
  68. data/lib/action_dispatch/http/parameter_filter.rb +25 -11
  69. data/lib/action_dispatch/http/parameters.rb +98 -39
  70. data/lib/action_dispatch/http/rack_cache.rb +2 -0
  71. data/lib/action_dispatch/http/request.rb +200 -118
  72. data/lib/action_dispatch/http/response.rb +225 -110
  73. data/lib/action_dispatch/http/upload.rb +12 -6
  74. data/lib/action_dispatch/http/url.rb +110 -28
  75. data/lib/action_dispatch/journey/formatter.rb +55 -32
  76. data/lib/action_dispatch/journey/gtg/builder.rb +7 -5
  77. data/lib/action_dispatch/journey/gtg/simulator.rb +3 -9
  78. data/lib/action_dispatch/journey/gtg/transition_table.rb +17 -16
  79. data/lib/action_dispatch/journey/nfa/builder.rb +5 -3
  80. data/lib/action_dispatch/journey/nfa/dot.rb +13 -13
  81. data/lib/action_dispatch/journey/nfa/simulator.rb +3 -1
  82. data/lib/action_dispatch/journey/nfa/transition_table.rb +5 -48
  83. data/lib/action_dispatch/journey/nodes/node.rb +18 -6
  84. data/lib/action_dispatch/journey/parser.rb +23 -22
  85. data/lib/action_dispatch/journey/parser.y +3 -2
  86. data/lib/action_dispatch/journey/parser_extras.rb +12 -4
  87. data/lib/action_dispatch/journey/path/pattern.rb +50 -44
  88. data/lib/action_dispatch/journey/route.rb +106 -28
  89. data/lib/action_dispatch/journey/router/utils.rb +20 -11
  90. data/lib/action_dispatch/journey/router.rb +35 -23
  91. data/lib/action_dispatch/journey/routes.rb +18 -16
  92. data/lib/action_dispatch/journey/scanner.rb +18 -15
  93. data/lib/action_dispatch/journey/visitors.rb +99 -52
  94. data/lib/action_dispatch/journey.rb +7 -5
  95. data/lib/action_dispatch/middleware/callbacks.rb +1 -2
  96. data/lib/action_dispatch/middleware/cookies.rb +304 -193
  97. data/lib/action_dispatch/middleware/debug_exceptions.rb +152 -57
  98. data/lib/action_dispatch/middleware/debug_locks.rb +124 -0
  99. data/lib/action_dispatch/middleware/exception_wrapper.rb +68 -69
  100. data/lib/action_dispatch/middleware/executor.rb +21 -0
  101. data/lib/action_dispatch/middleware/flash.rb +78 -54
  102. data/lib/action_dispatch/middleware/public_exceptions.rb +27 -25
  103. data/lib/action_dispatch/middleware/reloader.rb +5 -91
  104. data/lib/action_dispatch/middleware/remote_ip.rb +41 -31
  105. data/lib/action_dispatch/middleware/request_id.rb +17 -9
  106. data/lib/action_dispatch/middleware/session/abstract_store.rb +41 -25
  107. data/lib/action_dispatch/middleware/session/cache_store.rb +24 -14
  108. data/lib/action_dispatch/middleware/session/cookie_store.rb +72 -67
  109. data/lib/action_dispatch/middleware/session/mem_cache_store.rb +8 -2
  110. data/lib/action_dispatch/middleware/show_exceptions.rb +26 -22
  111. data/lib/action_dispatch/middleware/ssl.rb +114 -36
  112. data/lib/action_dispatch/middleware/stack.rb +31 -44
  113. data/lib/action_dispatch/middleware/static.rb +57 -50
  114. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb +2 -14
  115. data/lib/action_dispatch/middleware/templates/rescues/{_source.erb → _source.html.erb} +0 -0
  116. data/lib/action_dispatch/middleware/templates/rescues/_source.text.erb +8 -0
  117. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +21 -0
  118. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +13 -0
  119. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +1 -0
  120. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +1 -1
  121. data/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb +1 -1
  122. data/lib/action_dispatch/middleware/templates/routes/_route.html.erb +4 -4
  123. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +64 -64
  124. data/lib/action_dispatch/railtie.rb +19 -11
  125. data/lib/action_dispatch/request/session.rb +106 -59
  126. data/lib/action_dispatch/request/utils.rb +67 -24
  127. data/lib/action_dispatch/routing/endpoint.rb +9 -2
  128. data/lib/action_dispatch/routing/inspector.rb +58 -67
  129. data/lib/action_dispatch/routing/mapper.rb +733 -447
  130. data/lib/action_dispatch/routing/polymorphic_routes.rb +166 -140
  131. data/lib/action_dispatch/routing/redirection.rb +36 -26
  132. data/lib/action_dispatch/routing/route_set.rb +321 -291
  133. data/lib/action_dispatch/routing/routes_proxy.rb +32 -5
  134. data/lib/action_dispatch/routing/url_for.rb +65 -25
  135. data/lib/action_dispatch/routing.rb +17 -18
  136. data/lib/action_dispatch/system_test_case.rb +147 -0
  137. data/lib/action_dispatch/system_testing/browser.rb +49 -0
  138. data/lib/action_dispatch/system_testing/driver.rb +59 -0
  139. data/lib/action_dispatch/system_testing/server.rb +31 -0
  140. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +96 -0
  141. data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +31 -0
  142. data/lib/action_dispatch/system_testing/test_helpers/undef_methods.rb +26 -0
  143. data/lib/action_dispatch/testing/assertion_response.rb +47 -0
  144. data/lib/action_dispatch/testing/assertions/response.rb +45 -20
  145. data/lib/action_dispatch/testing/assertions/routing.rb +30 -26
  146. data/lib/action_dispatch/testing/assertions.rb +6 -4
  147. data/lib/action_dispatch/testing/integration.rb +348 -209
  148. data/lib/action_dispatch/testing/request_encoder.rb +55 -0
  149. data/lib/action_dispatch/testing/test_process.rb +28 -22
  150. data/lib/action_dispatch/testing/test_request.rb +27 -34
  151. data/lib/action_dispatch/testing/test_response.rb +35 -7
  152. data/lib/action_dispatch.rb +27 -19
  153. data/lib/action_pack/gem_version.rb +5 -3
  154. data/lib/action_pack/version.rb +3 -1
  155. data/lib/action_pack.rb +4 -2
  156. metadata +56 -38
  157. data/lib/action_controller/metal/hide_actions.rb +0 -40
  158. data/lib/action_controller/metal/rack_delegation.rb +0 -32
  159. data/lib/action_controller/middleware.rb +0 -39
  160. data/lib/action_controller/model_naming.rb +0 -12
  161. data/lib/action_dispatch/journey/backwards.rb +0 -5
  162. data/lib/action_dispatch/journey/router/strexp.rb +0 -27
  163. data/lib/action_dispatch/middleware/params_parser.rb +0 -60
  164. data/lib/action_dispatch/testing/assertions/dom.rb +0 -3
  165. data/lib/action_dispatch/testing/assertions/selector.rb +0 -3
  166. data/lib/action_dispatch/testing/assertions/tag.rb +0 -3
@@ -1,6 +1,9 @@
1
- require 'rack/session/abstract/id'
2
- require 'action_controller/metal/exceptions'
3
- require 'active_support/security_utils'
1
+ # frozen_string_literal: true
2
+
3
+ require "rack/session/abstract/id"
4
+ require "action_controller/metal/exceptions"
5
+ require "active_support/security_utils"
6
+ require "active_support/core_ext/string/strip"
4
7
 
5
8
  module ActionController #:nodoc:
6
9
  class InvalidAuthenticityToken < ActionControllerError #:nodoc:
@@ -13,9 +16,14 @@ module ActionController #:nodoc:
13
16
  # by including a token in the rendered HTML for your application. This token is
14
17
  # stored as a random string in the session, to which an attacker does not have
15
18
  # 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).
19
+ # token with the token in the session. All requests are checked except GET requests
20
+ # as these should be idempotent. Keep in mind that all session-oriented requests
21
+ # should be CSRF protected, including JavaScript and HTML requests.
22
+ #
23
+ # Since HTML and JavaScript requests are typically made from the browser, we
24
+ # need to ensure to verify request authenticity for the web browser. We can
25
+ # use session-oriented authentication for these types of requests, by using
26
+ # the <tt>protect_from_forgery</tt> method in our controllers.
19
27
  #
20
28
  # GET requests are not protected since they don't have side effects like writing
21
29
  # to the database and don't leak sensitive information. JavaScript requests are
@@ -26,22 +34,21 @@ module ActionController #:nodoc:
26
34
  # Ajax) requests are allowed to make GET requests for JavaScript responses.
27
35
  #
28
36
  # 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:
37
+ # you're building an API you should change forgery protection method in
38
+ # <tt>ApplicationController</tt> (by default: <tt>:exception</tt>):
30
39
  #
31
40
  # class ApplicationController < ActionController::Base
32
- # protect_from_forgery
33
- # skip_before_action :verify_authenticity_token, if: :json_request?
34
- #
35
- # protected
36
- #
37
- # def json_request?
38
- # request.format.json?
39
- # end
41
+ # protect_from_forgery unless: -> { request.format.json? }
40
42
  # end
41
43
  #
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.
44
+ # CSRF protection is turned on with the <tt>protect_from_forgery</tt> method.
45
+ # By default <tt>protect_from_forgery</tt> protects your session with
46
+ # <tt>:null_session</tt> method, which provides an empty session
47
+ # during request.
48
+ #
49
+ # We may want to disable CSRF protection for APIs since they are typically
50
+ # designed to be state-less. That is, the request API client will handle
51
+ # the session for you instead of Rails.
45
52
  #
46
53
  # The token parameter is named <tt>authenticity_token</tt> by default. The name and
47
54
  # value of this token must be added to every layout that renders forms by including
@@ -73,6 +80,22 @@ module ActionController #:nodoc:
73
80
  config_accessor :log_warning_on_csrf_failure
74
81
  self.log_warning_on_csrf_failure = true
75
82
 
83
+ # Controls whether the Origin header is checked in addition to the CSRF token.
84
+ config_accessor :forgery_protection_origin_check
85
+ self.forgery_protection_origin_check = false
86
+
87
+ # Controls whether form-action/method specific CSRF tokens are used.
88
+ config_accessor :per_form_csrf_tokens
89
+ self.per_form_csrf_tokens = false
90
+
91
+ # Controls whether forgery protection is enabled by default.
92
+ config_accessor :default_protect_from_forgery
93
+ self.default_protect_from_forgery = false
94
+
95
+ # Controls whether URL-safe CSRF tokens are generated.
96
+ config_accessor :urlsafe_csrf_tokens, instance_writer: false
97
+ self.urlsafe_csrf_tokens = false
98
+
76
99
  helper_method :form_authenticity_token
77
100
  helper_method :protect_against_forgery?
78
101
  end
@@ -86,13 +109,21 @@ module ActionController #:nodoc:
86
109
  #
87
110
  # class FooController < ApplicationController
88
111
  # protect_from_forgery except: :index
112
+ # end
113
+ #
114
+ # You can disable forgery protection on controller by skipping the verification before_action:
89
115
  #
90
- # You can disable CSRF protection on controller by skipping the verification before_action:
91
116
  # skip_before_action :verify_authenticity_token
92
117
  #
93
118
  # Valid Options:
94
119
  #
95
- # * <tt>:only/:except</tt> - Passed to the <tt>before_action</tt> call. Set which actions are verified.
120
+ # * <tt>:only/:except</tt> - Only apply forgery protection to a subset of actions. For example <tt>only: [ :create, :create_all ]</tt>.
121
+ # * <tt>:if/:unless</tt> - Turn off the forgery protection entirely depending on the passed Proc or method reference.
122
+ # * <tt>:prepend</tt> - By default, the verification of the authentication token will be added at the position of the
123
+ # protect_from_forgery call in your application. This means any callbacks added before are run first. This is useful
124
+ # when you want your forgery protection to depend on other callbacks, like authentication methods (Oauth vs Cookie auth).
125
+ #
126
+ # If you need to add verification to the beginning of the callback chain, use <tt>prepend: true</tt>.
96
127
  # * <tt>:with</tt> - Set the method to handle unverified request.
97
128
  #
98
129
  # Valid unverified request handling methods are:
@@ -100,19 +131,30 @@ module ActionController #:nodoc:
100
131
  # * <tt>:reset_session</tt> - Resets the session.
101
132
  # * <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.
102
133
  def protect_from_forgery(options = {})
134
+ options = options.reverse_merge(prepend: false)
135
+
103
136
  self.forgery_protection_strategy = protection_method_class(options[:with] || :null_session)
104
137
  self.request_forgery_protection_token ||= :authenticity_token
105
- prepend_before_action :verify_authenticity_token, options
138
+ before_action :verify_authenticity_token, options
106
139
  append_after_action :verify_same_origin_request
107
140
  end
108
141
 
142
+ # Turn off request forgery protection. This is a wrapper for:
143
+ #
144
+ # skip_before_action :verify_authenticity_token
145
+ #
146
+ # See +skip_before_action+ for allowed options.
147
+ def skip_forgery_protection(options = {})
148
+ skip_before_action :verify_authenticity_token, options
149
+ end
150
+
109
151
  private
110
152
 
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
153
+ def protection_method_class(name)
154
+ ActionController::RequestForgeryProtection::ProtectionMethods.const_get(name.to_s.classify)
155
+ rescue NameError
156
+ raise ArgumentError, "Invalid request forgery protection method, use :null_session, :exception, or :reset_session"
157
+ end
116
158
  end
117
159
 
118
160
  module ProtectionMethods
@@ -124,42 +166,34 @@ module ActionController #:nodoc:
124
166
  # This is the method that defines the application behavior when a request is found to be unverified.
125
167
  def handle_unverified_request
126
168
  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)
169
+ request.session = NullSessionHash.new(request)
170
+ request.flash = nil
171
+ request.session_options = { skip: true }
172
+ request.cookie_jar = NullCookieJar.build(request, {})
131
173
  end
132
174
 
133
- protected
175
+ private
134
176
 
135
- class NullSessionHash < Rack::Session::Abstract::SessionHash #:nodoc:
136
- def initialize(env)
137
- super(nil, env)
138
- @data = {}
139
- @loaded = true
140
- end
141
-
142
- # no-op
143
- def destroy; end
144
-
145
- def exists?
146
- true
147
- end
148
- end
177
+ class NullSessionHash < Rack::Session::Abstract::SessionHash #:nodoc:
178
+ def initialize(req)
179
+ super(nil, req)
180
+ @data = {}
181
+ @loaded = true
182
+ end
149
183
 
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?
184
+ # no-op
185
+ def destroy; end
155
186
 
156
- new(key_generator, host, secure, options_for_env({}))
187
+ def exists?
188
+ true
189
+ end
157
190
  end
158
191
 
159
- def write(*)
160
- # nothing
192
+ class NullCookieJar < ActionDispatch::Cookies::CookieJar #:nodoc:
193
+ def write(*)
194
+ # nothing
195
+ end
161
196
  end
162
- end
163
197
  end
164
198
 
165
199
  class ResetSession
@@ -183,29 +217,33 @@ module ActionController #:nodoc:
183
217
  end
184
218
  end
185
219
 
186
- protected
220
+ private
187
221
  # The actual before_action that is used to verify the CSRF token.
188
222
  # Don't override this directly. Provide your own forgery protection
189
223
  # strategy instead. If you override, you'll disable same-origin
190
- # `<script>` verification.
224
+ # <tt><script></tt> verification.
191
225
  #
192
226
  # Lean on the protect_from_forgery declaration to mark which actions are
193
227
  # due for same-origin request verification. If protect_from_forgery is
194
228
  # enabled on an action, this before_action flags its after_action to
195
229
  # verify that JavaScript responses are for XHR requests, ensuring they
196
230
  # follow the browser's same-origin policy.
197
- def verify_authenticity_token
231
+ def verify_authenticity_token # :doc:
198
232
  mark_for_same_origin_verification!
199
233
 
200
234
  if !verified_request?
201
235
  if logger && log_warning_on_csrf_failure
202
- logger.warn "Can't verify CSRF token authenticity"
236
+ if valid_request_origin?
237
+ logger.warn "Can't verify CSRF token authenticity."
238
+ else
239
+ logger.warn "HTTP Origin header (#{request.origin}) didn't match request.base_url (#{request.base_url})"
240
+ end
203
241
  end
204
242
  handle_unverified_request
205
243
  end
206
244
  end
207
245
 
208
- def handle_unverified_request
246
+ def handle_unverified_request # :doc:
209
247
  forgery_protection_strategy.new(self).handle_unverified_request
210
248
  end
211
249
 
@@ -215,30 +253,33 @@ module ActionController #:nodoc:
215
253
  "If you know what you're doing, go ahead and disable forgery " \
216
254
  "protection on this action to permit cross-origin JavaScript embedding."
217
255
  private_constant :CROSS_ORIGIN_JAVASCRIPT_WARNING
256
+ # :startdoc:
218
257
 
219
- # If `verify_authenticity_token` was run (indicating that we have
258
+ # If +verify_authenticity_token+ was run (indicating that we have
220
259
  # forgery protection enabled for this request) then also verify that
221
260
  # we aren't serving an unauthorized cross-origin response.
222
- def verify_same_origin_request
261
+ def verify_same_origin_request # :doc:
223
262
  if marked_for_same_origin_verification? && non_xhr_javascript_response?
224
- logger.warn CROSS_ORIGIN_JAVASCRIPT_WARNING if logger
263
+ if logger && log_warning_on_csrf_failure
264
+ logger.warn CROSS_ORIGIN_JAVASCRIPT_WARNING
265
+ end
225
266
  raise ActionController::InvalidCrossOriginRequest, CROSS_ORIGIN_JAVASCRIPT_WARNING
226
267
  end
227
268
  end
228
269
 
229
270
  # GET requests are checked for cross-origin JavaScript after rendering.
230
- def mark_for_same_origin_verification!
271
+ def mark_for_same_origin_verification! # :doc:
231
272
  @marked_for_same_origin_verification = request.get?
232
273
  end
233
274
 
234
- # If the `verify_authenticity_token` before_action ran, verify that
275
+ # If the +verify_authenticity_token+ before_action ran, verify that
235
276
  # JavaScript responses are only served to same-origin GET requests.
236
- def marked_for_same_origin_verification?
277
+ def marked_for_same_origin_verification? # :doc:
237
278
  @marked_for_same_origin_verification ||= false
238
279
  end
239
280
 
240
281
  # Check for cross-origin JavaScript responses.
241
- def non_xhr_javascript_response?
282
+ def non_xhr_javascript_response? # :doc:
242
283
  content_type =~ %r(\Atext/javascript) && !request.xhr?
243
284
  end
244
285
 
@@ -246,40 +287,57 @@ module ActionController #:nodoc:
246
287
 
247
288
  # Returns true or false if a request is verified. Checks:
248
289
  #
249
- # * is it a GET or HEAD request? Gets should be safe and idempotent
290
+ # * Is it a GET or HEAD request? GETs should be safe and idempotent
250
291
  # * 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?
292
+ # * Does the X-CSRF-Token header match the form_authenticity_token?
293
+ def verified_request? # :doc:
253
294
  !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'])
295
+ (valid_request_origin? && any_authenticity_token_valid?)
296
+ end
297
+
298
+ # Checks if any of the authenticity tokens from the request are valid.
299
+ def any_authenticity_token_valid? # :doc:
300
+ request_authenticity_tokens.any? do |token|
301
+ valid_authenticity_token?(session, token)
302
+ end
303
+ end
304
+
305
+ # Possible authenticity tokens sent in the request.
306
+ def request_authenticity_tokens # :doc:
307
+ [form_authenticity_param, request.x_csrf_token]
256
308
  end
257
309
 
258
310
  # Sets the token value for the current session.
259
- def form_authenticity_token
260
- masked_authenticity_token(session)
311
+ def form_authenticity_token(form_options: {})
312
+ masked_authenticity_token(session, form_options: form_options)
261
313
  end
262
314
 
263
315
  # Creates a masked version of the authenticity token that varies
264
316
  # on each request. The masking is used to mitigate SSL attacks
265
317
  # 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)
318
+ def masked_authenticity_token(session, form_options: {}) # :doc:
319
+ action, method = form_options.values_at(:action, :method)
320
+
321
+ raw_token = if per_form_csrf_tokens && action && method
322
+ action_path = normalize_action_path(action)
323
+ per_form_csrf_token(session, action_path, method)
324
+ else
325
+ global_csrf_token(session)
326
+ end
327
+
328
+ mask_token(raw_token)
271
329
  end
272
330
 
273
331
  # Checks the client's masked token to see if it matches the
274
332
  # session token. Essentially the inverse of
275
333
  # +masked_authenticity_token+.
276
- def valid_authenticity_token?(session, encoded_masked_token)
334
+ def valid_authenticity_token?(session, encoded_masked_token) # :doc:
277
335
  if encoded_masked_token.nil? || encoded_masked_token.empty? || !encoded_masked_token.is_a?(String)
278
336
  return false
279
337
  end
280
338
 
281
339
  begin
282
- masked_token = Base64.strict_decode64(encoded_masked_token)
340
+ masked_token = decode_csrf_token(encoded_masked_token)
283
341
  rescue ArgumentError # encoded_masked_token is invalid Base64
284
342
  return false
285
343
  end
@@ -291,44 +349,173 @@ module ActionController #:nodoc:
291
349
  if masked_token.length == AUTHENTICITY_TOKEN_LENGTH
292
350
  # This is actually an unmasked token. This is expected if
293
351
  # you have just upgraded to masked tokens, but should stop
294
- # happening shortly after installing this gem
352
+ # happening shortly after installing this gem.
295
353
  compare_with_real_token masked_token, session
296
354
 
297
355
  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)
356
+ csrf_token = unmask_token(masked_token)
357
+
358
+ compare_with_global_token(csrf_token, session) ||
359
+ compare_with_real_token(csrf_token, session) ||
360
+ valid_per_form_csrf_token?(csrf_token, session)
361
+ else
362
+ false # Token is malformed.
363
+ end
364
+ end
365
+
366
+ def unmask_token(masked_token) # :doc:
367
+ # Split the token into the one-time pad and the encrypted
368
+ # value and decrypt it.
369
+ one_time_pad = masked_token[0...AUTHENTICITY_TOKEN_LENGTH]
370
+ encrypted_csrf_token = masked_token[AUTHENTICITY_TOKEN_LENGTH..-1]
371
+ xor_byte_strings(one_time_pad, encrypted_csrf_token)
372
+ end
373
+
374
+ def mask_token(raw_token) # :doc:
375
+ one_time_pad = SecureRandom.random_bytes(AUTHENTICITY_TOKEN_LENGTH)
376
+ encrypted_csrf_token = xor_byte_strings(one_time_pad, raw_token)
377
+ masked_token = one_time_pad + encrypted_csrf_token
378
+ encode_csrf_token(masked_token)
379
+ end
380
+
381
+ def compare_with_real_token(token, session) # :doc:
382
+ ActiveSupport::SecurityUtils.fixed_length_secure_compare(token, real_csrf_token(session))
383
+ end
384
+
385
+ def compare_with_global_token(token, session) # :doc:
386
+ ActiveSupport::SecurityUtils.fixed_length_secure_compare(token, global_csrf_token(session))
387
+ end
303
388
 
304
- compare_with_real_token csrf_token, session
389
+ def valid_per_form_csrf_token?(token, session) # :doc:
390
+ if per_form_csrf_tokens
391
+ correct_token = per_form_csrf_token(
392
+ session,
393
+ normalize_action_path(request.fullpath),
394
+ request.request_method
395
+ )
305
396
 
397
+ ActiveSupport::SecurityUtils.fixed_length_secure_compare(token, correct_token)
306
398
  else
307
- false # Token is malformed
399
+ false
308
400
  end
309
401
  end
310
402
 
311
- def compare_with_real_token(token, session)
312
- ActiveSupport::SecurityUtils.secure_compare(token, real_csrf_token(session))
403
+ def real_csrf_token(session) # :doc:
404
+ session[:_csrf_token] ||= generate_csrf_token
405
+ decode_csrf_token(session[:_csrf_token])
406
+ end
407
+
408
+ def per_form_csrf_token(session, action_path, method) # :doc:
409
+ csrf_token_hmac(session, [action_path, method.downcase].join("#"))
313
410
  end
314
411
 
315
- def real_csrf_token(session)
316
- session[:_csrf_token] ||= SecureRandom.base64(AUTHENTICITY_TOKEN_LENGTH)
317
- Base64.strict_decode64(session[:_csrf_token])
412
+ GLOBAL_CSRF_TOKEN_IDENTIFIER = "!real_csrf_token"
413
+ private_constant :GLOBAL_CSRF_TOKEN_IDENTIFIER
414
+
415
+ def global_csrf_token(session) # :doc:
416
+ csrf_token_hmac(session, GLOBAL_CSRF_TOKEN_IDENTIFIER)
417
+ end
418
+
419
+ def csrf_token_hmac(session, identifier) # :doc:
420
+ OpenSSL::HMAC.digest(
421
+ OpenSSL::Digest::SHA256.new,
422
+ real_csrf_token(session),
423
+ identifier
424
+ )
318
425
  end
319
426
 
320
- def xor_byte_strings(s1, s2)
321
- s1.bytes.zip(s2.bytes).map { |(c1,c2)| c1 ^ c2 }.pack('c*')
427
+ def xor_byte_strings(s1, s2) # :doc:
428
+ s2_bytes = s2.bytes
429
+ s1.each_byte.with_index { |c1, i| s2_bytes[i] ^= c1 }
430
+ s2_bytes.pack("C*")
322
431
  end
323
432
 
324
433
  # The form's authenticity parameter. Override to provide your own.
325
- def form_authenticity_param
434
+ def form_authenticity_param # :doc:
326
435
  params[request_forgery_protection_token]
327
436
  end
328
437
 
329
438
  # Checks if the controller allows forgery protection.
330
- def protect_against_forgery?
439
+ def protect_against_forgery? # :doc:
331
440
  allow_forgery_protection
332
441
  end
442
+
443
+ NULL_ORIGIN_MESSAGE = <<-MSG.strip_heredoc
444
+ The browser returned a 'null' origin for a request with origin-based forgery protection turned on. This usually
445
+ means you have the 'no-referrer' Referrer-Policy header enabled, or that the request came from a site that
446
+ refused to give its origin. This makes it impossible for Rails to verify the source of the requests. Likely the
447
+ best solution is to change your referrer policy to something less strict like same-origin or strict-same-origin.
448
+ If you cannot change the referrer policy, you can disable origin checking with the
449
+ Rails.application.config.action_controller.forgery_protection_origin_check setting.
450
+ MSG
451
+
452
+ # Checks if the request originated from the same origin by looking at the
453
+ # Origin header.
454
+ def valid_request_origin? # :doc:
455
+ if forgery_protection_origin_check
456
+ # We accept blank origin headers because some user agents don't send it.
457
+ raise InvalidAuthenticityToken, NULL_ORIGIN_MESSAGE if request.origin == "null"
458
+ request.origin.nil? || request.origin == request.base_url
459
+ else
460
+ true
461
+ end
462
+ end
463
+
464
+ def normalize_action_path(action_path) # :doc:
465
+ uri = URI.parse(action_path)
466
+ uri.path.chomp("/")
467
+ end
468
+
469
+ def generate_csrf_token # :nodoc:
470
+ if urlsafe_csrf_tokens
471
+ SecureRandom.urlsafe_base64(AUTHENTICITY_TOKEN_LENGTH, padding: false)
472
+ else
473
+ SecureRandom.base64(AUTHENTICITY_TOKEN_LENGTH)
474
+ end
475
+ end
476
+
477
+ if RUBY_VERSION.start_with?("2.2")
478
+ # Backported https://github.com/ruby/ruby/commit/6b6680945ed3274cddbc34fdfd410d74081a3e94
479
+ using Module.new {
480
+ refine Base64.singleton_class do
481
+ def urlsafe_encode64(bin, padding: true)
482
+ str = strict_encode64(bin).tr("+/", "-_")
483
+ str = str.delete("=") unless padding
484
+ str
485
+ end
486
+
487
+ def urlsafe_decode64(str)
488
+ # NOTE: RFC 4648 does say nothing about unpadded input, but says that
489
+ # "the excess pad characters MAY also be ignored", so it is inferred that
490
+ # unpadded input is also acceptable.
491
+ str = str.tr("-_", "+/")
492
+ if !str.end_with?("=") && str.length % 4 != 0
493
+ str = str.ljust((str.length + 3) & ~3, "=")
494
+ end
495
+ strict_decode64(str)
496
+ end
497
+ end
498
+ }
499
+ end
500
+
501
+ def encode_csrf_token(csrf_token) # :nodoc:
502
+ if urlsafe_csrf_tokens
503
+ Base64.urlsafe_encode64(csrf_token, padding: false)
504
+ else
505
+ Base64.strict_encode64(csrf_token)
506
+ end
507
+ end
508
+
509
+ def decode_csrf_token(encoded_csrf_token) # :nodoc:
510
+ if urlsafe_csrf_tokens
511
+ Base64.urlsafe_decode64(encoded_csrf_token)
512
+ else
513
+ begin
514
+ Base64.strict_decode64(encoded_csrf_token)
515
+ rescue ArgumentError
516
+ Base64.urlsafe_decode64(encoded_csrf_token)
517
+ end
518
+ end
519
+ end
333
520
  end
334
521
  end
@@ -1,25 +1,18 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActionController #:nodoc:
2
- # This module is responsible to provide `rescue_from` helpers
3
- # to controllers and configure when detailed exceptions must be
4
+ # This module is responsible for providing +rescue_from+ helpers
5
+ # to controllers and configuring when detailed exceptions must be
4
6
  # shown.
5
7
  module Rescue
6
8
  extend ActiveSupport::Concern
7
9
  include ActiveSupport::Rescuable
8
10
 
9
- def rescue_with_handler(exception)
10
- if (exception.respond_to?(:original_exception) &&
11
- (orig_exception = exception.original_exception) &&
12
- handler_for_rescue(orig_exception))
13
- exception = orig_exception
14
- end
15
- super(exception)
16
- end
17
-
18
11
  # Override this method if you want to customize when detailed
19
12
  # exceptions must be shown. This method is only called when
20
- # consider_all_requests_local is false. By default, it returns
21
- # false, but someone may set it to `request.local?` so local
22
- # requests in production still shows the detailed exception pages.
13
+ # +consider_all_requests_local+ is +false+. By default, it returns
14
+ # +false+, but someone may set it to <tt>request.local?</tt> so local
15
+ # requests in production still show the detailed exception pages.
23
16
  def show_detailed_exceptions?
24
17
  false
25
18
  end
@@ -28,8 +21,8 @@ module ActionController #:nodoc:
28
21
  def process_action(*args)
29
22
  super
30
23
  rescue Exception => exception
31
- request.env['action_dispatch.show_detailed_exceptions'] ||= show_detailed_exceptions?
32
- rescue_with_handler(exception) || raise(exception)
24
+ request.env["action_dispatch.show_detailed_exceptions"] ||= show_detailed_exceptions?
25
+ rescue_with_handler(exception) || raise
33
26
  end
34
27
  end
35
28
  end
@@ -1,9 +1,11 @@
1
- require 'rack/chunked'
1
+ # frozen_string_literal: true
2
+
3
+ require "rack/chunked"
2
4
 
3
5
  module ActionController #:nodoc:
4
6
  # Allows views to be streamed back to the client as they are rendered.
5
7
  #
6
- # The default way Rails renders views is by first rendering the template
8
+ # By default, Rails renders views by first rendering the template
7
9
  # and then the layout. The response is sent to the client after the whole
8
10
  # template is rendered, all queries are made, and the layout is processed.
9
11
  #
@@ -110,9 +112,9 @@ module ActionController #:nodoc:
110
112
  # This means that, if you have <code>yield :title</code> in your layout
111
113
  # and you want to use streaming, you would have to render the whole template
112
114
  # (and eventually trigger all queries) before streaming the title and all
113
- # assets, which kills the purpose of streaming. For this reason Rails 3.1
114
- # introduces a new helper called +provide+ that does the same as +content_for+
115
- # but tells the layout to stop searching for other entries and continue rendering.
115
+ # assets, which kills the purpose of streaming. For this purpose, you can use
116
+ # a helper called +provide+ that does the same as +content_for+ but tells the
117
+ # layout to stop searching for other entries and continue rendering.
116
118
  #
117
119
  # For instance, the template above using +provide+ would be:
118
120
  #
@@ -181,7 +183,7 @@ module ActionController #:nodoc:
181
183
  # unicorn_rails --config-file unicorn.config.rb
182
184
  #
183
185
  # You may also want to configure other parameters like <tt>:tcp_nodelay</tt>.
184
- # Please check its documentation for more information: http://unicorn.bogomips.org/Unicorn/Configurator.html#method-i-listen
186
+ # Please check its documentation for more information: https://bogomips.org/unicorn/Unicorn/Configurator.html#method-i-listen
185
187
  #
186
188
  # If you are using Unicorn with NGINX, you may need to tweak NGINX.
187
189
  # Streaming should work out of the box on Rainbows.
@@ -193,13 +195,13 @@ module ActionController #:nodoc:
193
195
  module Streaming
194
196
  extend ActiveSupport::Concern
195
197
 
196
- protected
198
+ private
197
199
 
198
200
  # Set proper cache control and transfer encoding when streaming
199
- def _process_options(options) #:nodoc:
201
+ def _process_options(options)
200
202
  super
201
203
  if options[:stream]
202
- if env["HTTP_VERSION"] == "HTTP/1.0"
204
+ if request.version == "HTTP/1.0"
203
205
  options.delete(:stream)
204
206
  else
205
207
  headers["Cache-Control"] ||= "no-cache"
@@ -210,7 +212,7 @@ module ActionController #:nodoc:
210
212
  end
211
213
 
212
214
  # Call render_body if we are streaming instead of usual +render+.
213
- def _render_template(options) #:nodoc:
215
+ def _render_template(options)
214
216
  if options.delete(:stream)
215
217
  Rack::Chunked::Body.new view_renderer.render_body(view_context, options)
216
218
  else