@@ -239,7 +239,7 @@ def encryptor_class
239239 encryptor = new_encryptor ( @secret , purpose : 'testing' , pad_size : 24 )
240240 message = encryptor . encrypt ( { 'foo' => 'bar' * 4 } )
241241
242- decoded_message = Base64 . strict_decode64 ( message )
242+ decoded_message = Base64 . urlsafe_decode64 ( message )
243243
244244 # slice 1 byte for version, 32 bytes for cipher_secret, 12 bytes for IV,
245245 # 16 bytes for the auth tag from the start of the string
@@ -252,9 +252,9 @@ def encryptor_class
252252 encryptor = new_encryptor ( @secret , purpose : 'testing' )
253253 message = encryptor . encrypt ( { 'foo' => 'bar' } )
254254
255- decoded_message = Base64 . strict_decode64 ( message )
255+ decoded_message = Base64 . urlsafe_decode64 ( message )
256256 decoded_message [ 0 ] = "\1 "
257- reencoded_message = Base64 . strict_encode64 ( decoded_message )
257+ reencoded_message = Base64 . urlsafe_encode64 ( decoded_message )
258258
259259 -> { encryptor . decrypt ( reencoded_message ) } . must_raise Rack ::Session ::Encryptor ::InvalidMessage
260260 end
@@ -267,7 +267,7 @@ def encryptor_class
267267 encryptor = new_encryptor ( @secret )
268268
269269 message = encryptor . encrypt ( { 'foo' => 'bar' } )
270- raw_message = Base64 . strict_decode64 ( message )
270+ raw_message = Base64 . urlsafe_decode64 ( message )
271271
272272 version = raw_message . slice! ( 0 , 1 )
273273 salt = raw_message . slice! ( 0 , 32 )
@@ -287,7 +287,7 @@ def encryptor_class
287287 encryptor = new_encryptor ( @secret , purpose : 'testing' , pad_size : 24 )
288288 message = encryptor . encrypt ( { 'foo' => 'bar' } )
289289
290- message_key = Base64 . strict_decode64 ( message ) . slice ( 1 , 32 )
290+ message_key = Base64 . urlsafe_decode64 ( message ) . slice ( 1 , 32 )
291291
292292 callable = proc do |cipher , key |
293293 key . wont_equal @secret
@@ -334,7 +334,7 @@ def encryptor_class
334334 encryptor = Rack ::Session ::Encryptor . new ( @secret , { mode : :not_v1 } )
335335
336336 encrypted_message = encryptor . encrypt ( { 'foo' => 'bar' } )
337- version = Base64 . strict_decode64 ( encrypted_message ) [ 0 ]
337+ version = Base64 . urlsafe_decode64 ( encrypted_message ) [ 0 ]
338338
339339 version . must_equal "\2 "
340340 end
@@ -373,5 +373,25 @@ def encryptor_class
373373 decrypted_message_v1 . must_equal ( { 'foo' => 'bar' } )
374374 decrypted_message_v2 . must_equal ( { 'foo' => 'bar' } )
375375 end
376+
377+ # Rack's parse_cookies_header applies URI.decode_www_form_component to
378+ # cookie values, which converts '+' to space. Standard Base64
379+ # (strict_encode64) can produce '+' characters, which would corrupt the
380+ # cookie before decryption. V2 must use URL-safe Base64 to avoid this.
381+ it 'decrypts V2 messages that have passed through Rack cookie parsing' do
382+ encryptor = Rack ::Session ::Encryptor . new ( @secret , { mode : :v2 } )
383+ encrypted_message = encryptor . encrypt ( { 'foo' => 'bar' } )
384+
385+ # V2 output must only contain URL-safe Base64 characters; '+' and '/'
386+ # are the characters that strict_encode64 produces but urlsafe_encode64
387+ # does not, and which Rack's cookie parser would corrupt.
388+ encrypted_message . must_match ( /\A [A-Za-z0-9\- _=]+\z / )
389+
390+ # Simulate what Rack::Utils.parse_cookies_header does to cookie values
391+ cookie_value_after_rack = URI . decode_www_form_component ( encrypted_message )
392+ cookie_value_after_rack . must_equal encrypted_message
393+
394+ encryptor . decrypt ( cookie_value_after_rack ) . must_equal ( { 'foo' => 'bar' } )
395+ end
376396 end
377397end
0 commit comments