CSRF Protection in Rails (4-2-stable)
Generate
In rails, we can find a csrf token in the head of html or a hidden input of forms:
<html>
<head>
<meta name= "csrf-token"
content= "V6CZMZGA+lnkmonZbw74c81KAGjBAlK6Jk1UEbQkW95zjIMxU/G5ixcbokIG5GIzlG2gwtag4la/eLoTXT6/Dw==" />
</head>
<body>
<form>
<input type= "hidden"
name= "authenticity_token"
value= "OFkGlN+ho64ffPlNEJ7P01VPO+dC7yQ96rl13pKNV8McdRyUHdDgfOz90tZ5dFWTDGibTVVNlNFzjJvce5ezEg==" />
</form>
</body>
</html>
Those tokens are generated in ActionController
and not predictable (that’s what we need for csrf protection).
In ActionView module we can find how csrf meta tags are generated:
def csrf_meta_tags
if protect_against_forgery?
[
tag ( 'meta' , :name => 'csrf-param' , :content => request_forgery_protection_token ),
tag ( 'meta' , :name => 'csrf-token' , :content => form_authenticity_token )
]. join ( " \n " ). html_safe
end
end
The methods form_authenticity_token is in ActionController :
AUTHENTICITY_TOKEN_LENGTH = 32
def form_authenticity_token
masked_authenticity_token ( session )
end
def masked_authenticity_token ( session )
one_time_pad = SecureRandom . random_bytes ( AUTHENTICITY_TOKEN_LENGTH )
encrypted_csrf_token = xor_byte_strings ( one_time_pad , real_csrf_token ( session ))
masked_token = one_time_pad + encrypted_csrf_token
Base64 . strict_encode64 ( masked_token )
end
def valid_authenticity_token? ( session , encoded_masked_token )
return false if encoded_masked_token . nil? || encoded_masked_token . empty?
begin
masked_token = Base64 . strict_decode64 ( encoded_masked_token )
rescue ArgumentError # encoded_masked_token is invalid Base64
return false
end
# See if it's actually a masked token or not. In order to
# deploy this code, we should be able to handle any unmasked
# tokens that we've issued without error.
if masked_token . length == AUTHENTICITY_TOKEN_LENGTH
# This is actually an unmasked token. This is expected if
# you have just upgraded to masked tokens, but should stop
# happening shortly after installing this gem
compare_with_real_token masked_token , session
elsif masked_token . length == AUTHENTICITY_TOKEN_LENGTH * 2
# Split the token into the one-time pad and the encrypted
# value and decrypt it
one_time_pad = masked_token [ 0 ... AUTHENTICITY_TOKEN_LENGTH ]
encrypted_csrf_token = masked_token [ AUTHENTICITY_TOKEN_LENGTH ..- 1 ]
csrf_token = xor_byte_strings ( one_time_pad , encrypted_csrf_token )
compare_with_real_token csrf_token , session
else
false # Token is malformed
end
end
def real_csrf_token ( session )
session [ :_csrf_token ] ||= SecureRandom . base64 ( AUTHENTICITY_TOKEN_LENGTH )
Base64 . strict_decode64 ( session [ :_csrf_token ])
end
In brief, a token is generated by this steps:
For each session, a random 32 bytes long token in base 64 is generated (44 long).
Decode this token with base64 (to 32 bytes binary token).
Everytime when we need a form authenticity token, a 32 bytes one time pad is generated.
XOR one time pad and csrf token to encrypted_csrf_token.
Concatenate csrf token and encrypted_csrf_token (32 + 32 = 64 bytes).
Encode the result in base64 give us the final csrf token (64 * (4/3) = 88 bytes).
Code tracing verification
Now open a rails site in browser and find the csrf token, refresh the page to get 2 different tokens.
In rails console def a decode method and pass those tokens to see the result. The result should be the same.
# extract from valid_authenticity_token?
def decode ( token )
token = Base64 . strict_decode64 token
t1 = token [ 0 ... 32 ]
t2 = token [ 32 ..- 1 ]
t1 . bytes . zip ( t2 . bytes ). map { | ( c1 , c2 ) | c1 ^ c2 }. pack ( 'c*' )
end
Then re-launch the browser and decode another token, the result should be changed (bound to session!).
CSRF Protection in Nodejs
In CSURF middleware , a secret is generated
using secretSync method of csrf module which return
pseudo random bytes.
// generate & set new secret
if ( sec === undefined ) {
sec = tokens . secretSync ()
setsecret ( req , res , sec , cookie )
}
Then in csrf module , use tokenize method to generate
the final token:
csrfTokens . tokenize = function tokenize ( secret , salt ) {
var hash = escape ( crypto
. createHash ( 'sha1' )
. update ( salt )
. update ( '-' )
. update ( secret )
. digest ( 'base64' ))
return salt + '-' + hash
}
To verify the token, re-generate the token with salt and secret and compare the result. As in the
tokenize method, salt is in the beginning of the final token and hash is generated base on salt and
the pseudo randome secret.
verify : function verify ( secret , token ) {
if ( ! secret || typeof secret !== 'string' ) {
return false
}
if ( ! token || typeof token !== 'string' ) {
return false
}
var index = token . indexOf ( '-' )
if ( index === - 1 ) {
return false
}
var salt = token . substr ( 0 , index )
var expected = tokenize ( secret , salt )
return scmp ( token , expected )
}
Code tracing verification
Can not verify if we don’t known the secret.
References