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