@@ -55,6 +55,8 @@ class InvalidCrossOriginRequest < ActionControllerError # :nodoc:
55
55
# Learn more about CSRF attacks and securing your application in the
56
56
# {Ruby on Rails Security Guide}[https://guides.rubyonrails.org/security.html].
57
57
module RequestForgeryProtection
58
+ CSRF_TOKEN = "action_controller.csrf_token"
59
+
58
60
extend ActiveSupport ::Concern
59
61
60
62
include AbstractController ::Helpers
@@ -90,6 +92,10 @@ module RequestForgeryProtection
90
92
config_accessor :default_protect_from_forgery
91
93
self . default_protect_from_forgery = false
92
94
95
+ # The strategy to use for storing and retrieving CSRF tokens.
96
+ config_accessor :csrf_token_storage_strategy
97
+ self . csrf_token_storage_strategy = SessionStore . new
98
+
93
99
helper_method :form_authenticity_token
94
100
helper_method :protect_against_forgery?
95
101
end
@@ -140,11 +146,39 @@ module ClassMethods
140
146
# class ApplicationController < ActionController:x:Base
141
147
# protect_from_forgery with: CustomStrategy
142
148
# end
149
+ # * <tt>:store</tt> - Set the strategy to store and retrieve CSRF tokens.
150
+ #
151
+ # Built-in session token strategies are:
152
+ # * <tt>:session</tt> - Store the CSRF token in the session. Used as default if <tt>:store</tt> option is not specified.
153
+ # * <tt>:cookie</tt> - Store the CSRF token in an encrypted cookie.
154
+ #
155
+ # You can also implement custom strategy classes for CSRF token storage:
156
+ #
157
+ # class CustomStore
158
+ # def fetch(request)
159
+ # # Return the token from a custom location
160
+ # end
161
+ #
162
+ # def store(request, csrf_token)
163
+ # # Store the token in a custom location
164
+ # end
165
+ #
166
+ # def reset(request)
167
+ # # Delete the stored session token
168
+ # end
169
+ # end
170
+ #
171
+ # class ApplicationController < ActionController:x:Base
172
+ # protect_from_forgery store: CustomStore.new
173
+ # end
143
174
def protect_from_forgery ( options = { } )
144
175
options = options . reverse_merge ( prepend : false )
145
176
146
177
self . forgery_protection_strategy = protection_method_class ( options [ :with ] || :null_session )
147
178
self . request_forgery_protection_token ||= :authenticity_token
179
+
180
+ self . csrf_token_storage_strategy = storage_strategy ( options [ :store ] || SessionStore . new )
181
+
148
182
before_action :verify_authenticity_token , options
149
183
append_after_action :verify_same_origin_request
150
184
end
@@ -173,6 +207,22 @@ def protection_method_class(name)
173
207
raise ArgumentError , "Invalid request forgery protection method, use :null_session, :exception, :reset_session, or a custom forgery protection class."
174
208
end
175
209
end
210
+
211
+ def storage_strategy ( name )
212
+ case name
213
+ when :session
214
+ SessionStore . new
215
+ when :cookie
216
+ CookieStore . new ( :csrf_token )
217
+ else
218
+ return name if is_storage_strategy? ( name )
219
+ raise ArgumentError , "Invalid CSRF token storage strategy, use :session, :cookie, or a custom CSRF token storage class."
220
+ end
221
+ end
222
+
223
+ def is_storage_strategy? ( object )
224
+ object . respond_to? ( :fetch ) && object . respond_to? ( :store )
225
+ end
176
226
end
177
227
178
228
module ProtectionMethods
@@ -240,6 +290,63 @@ def handle_unverified_request
240
290
end
241
291
end
242
292
293
+ class SessionStore
294
+ def fetch ( request )
295
+ request . session [ :_csrf_token ]
296
+ end
297
+
298
+ def store ( request , csrf_token )
299
+ request . session [ :_csrf_token ] = csrf_token
300
+ end
301
+
302
+ def reset ( request )
303
+ request . session . delete ( :_csrf_token )
304
+ end
305
+ end
306
+
307
+ class CookieStore
308
+ def initialize ( cookie = :csrf_token )
309
+ @cookie_name = cookie
310
+ end
311
+
312
+ def fetch ( request )
313
+ contents = request . cookie_jar . encrypted [ @cookie_name ]
314
+ return nil if contents . nil?
315
+
316
+ value = JSON . parse ( contents )
317
+ return nil unless value . dig ( "session_id" , "public_id" ) == request . session . id_was &.public_id
318
+
319
+ value [ "token" ]
320
+ rescue JSON ::ParserError
321
+ nil
322
+ end
323
+
324
+ def store ( request , csrf_token )
325
+ request . cookie_jar . encrypted . permanent [ @cookie_name ] = {
326
+ value : {
327
+ token : csrf_token ,
328
+ session_id : request . session . id ,
329
+ } . to_json ,
330
+ httponly : true ,
331
+ same_site : :lax ,
332
+ }
333
+ end
334
+
335
+ def reset ( request )
336
+ request . cookie_jar . delete ( @cookie_name )
337
+ end
338
+ end
339
+
340
+ def reset_csrf_token ( request ) # :doc:
341
+ request . env . delete ( CSRF_TOKEN )
342
+ csrf_token_storage_strategy . reset ( request )
343
+ end
344
+
345
+ def commit_csrf_token ( request ) # :doc:
346
+ csrf_token = request . env [ CSRF_TOKEN ]
347
+ csrf_token_storage_strategy . store ( request , csrf_token ) unless csrf_token . nil?
348
+ end
349
+
243
350
private
244
351
# The actual before_action that is used to verify the CSRF token.
245
352
# Don't override this directly. Provide your own forgery protection
@@ -341,20 +448,20 @@ def request_authenticity_tokens # :doc:
341
448
342
449
# Creates the authenticity token for the current request.
343
450
def form_authenticity_token ( form_options : { } ) # :doc:
344
- masked_authenticity_token ( session , form_options : form_options )
451
+ masked_authenticity_token ( form_options : form_options )
345
452
end
346
453
347
454
# Creates a masked version of the authenticity token that varies
348
455
# on each request. The masking is used to mitigate SSL attacks
349
456
# like BREACH.
350
- def masked_authenticity_token ( session , form_options : { } )
457
+ def masked_authenticity_token ( form_options : { } )
351
458
action , method = form_options . values_at ( :action , :method )
352
459
353
460
raw_token = if per_form_csrf_tokens && action && method
354
461
action_path = normalize_action_path ( action )
355
- per_form_csrf_token ( session , action_path , method )
462
+ per_form_csrf_token ( nil , action_path , method )
356
463
else
357
- global_csrf_token ( session )
464
+ global_csrf_token
358
465
end
359
466
360
467
mask_token ( raw_token )
@@ -382,14 +489,14 @@ def valid_authenticity_token?(session, encoded_masked_token) # :doc:
382
489
# This is actually an unmasked token. This is expected if
383
490
# you have just upgraded to masked tokens, but should stop
384
491
# happening shortly after installing this gem.
385
- compare_with_real_token masked_token , session
492
+ compare_with_real_token masked_token
386
493
387
494
elsif masked_token . length == AUTHENTICITY_TOKEN_LENGTH * 2
388
495
csrf_token = unmask_token ( masked_token )
389
496
390
- compare_with_global_token ( csrf_token , session ) ||
391
- compare_with_real_token ( csrf_token , session ) ||
392
- valid_per_form_csrf_token? ( csrf_token , session )
497
+ compare_with_global_token ( csrf_token ) ||
498
+ compare_with_real_token ( csrf_token ) ||
499
+ valid_per_form_csrf_token? ( csrf_token )
393
500
else
394
501
false # Token is malformed.
395
502
end
@@ -410,15 +517,15 @@ def mask_token(raw_token) # :doc:
410
517
encode_csrf_token ( masked_token )
411
518
end
412
519
413
- def compare_with_real_token ( token , session ) # :doc:
520
+ def compare_with_real_token ( token , session = nil ) # :doc:
414
521
ActiveSupport ::SecurityUtils . fixed_length_secure_compare ( token , real_csrf_token ( session ) )
415
522
end
416
523
417
- def compare_with_global_token ( token , session ) # :doc:
524
+ def compare_with_global_token ( token , session = nil ) # :doc:
418
525
ActiveSupport ::SecurityUtils . fixed_length_secure_compare ( token , global_csrf_token ( session ) )
419
526
end
420
527
421
- def valid_per_form_csrf_token? ( token , session ) # :doc:
528
+ def valid_per_form_csrf_token? ( token , session = nil ) # :doc:
422
529
if per_form_csrf_tokens
423
530
correct_token = per_form_csrf_token (
424
531
session ,
@@ -432,9 +539,12 @@ def valid_per_form_csrf_token?(token, session) # :doc:
432
539
end
433
540
end
434
541
435
- def real_csrf_token ( session ) # :doc:
436
- session [ :_csrf_token ] ||= generate_csrf_token
437
- decode_csrf_token ( session [ :_csrf_token ] )
542
+ def real_csrf_token ( _session = nil ) # :doc:
543
+ csrf_token = request . env . fetch ( CSRF_TOKEN ) do
544
+ request . env [ CSRF_TOKEN ] = csrf_token_storage_strategy . fetch ( request ) || generate_csrf_token
545
+ end
546
+
547
+ decode_csrf_token ( csrf_token )
438
548
end
439
549
440
550
def per_form_csrf_token ( session , action_path , method ) # :doc:
@@ -444,7 +554,7 @@ def per_form_csrf_token(session, action_path, method) # :doc:
444
554
GLOBAL_CSRF_TOKEN_IDENTIFIER = "!real_csrf_token"
445
555
private_constant :GLOBAL_CSRF_TOKEN_IDENTIFIER
446
556
447
- def global_csrf_token ( session ) # :doc:
557
+ def global_csrf_token ( session = nil ) # :doc:
448
558
csrf_token_hmac ( session , GLOBAL_CSRF_TOKEN_IDENTIFIER )
449
559
end
450
560
0 commit comments