skip to content

CSRF tokens in Symfony

What are CSRF tokens and how they worked in Sf1.4 in depth

What is that CSRF thing?

CSRF, or Cross-site request forgery, is a form of attack that takes advantage on the trust that a web application has on the user’s browser. If a different site than yours makes a user do a request to your site, since the user has a cookie session for your site, if you don’t have further protection, your application will trust the user’s browser and execute the request. But the user is not doing it voluntarily.

Wikipedia explains it better, but basically if you are logged in into your electronic banking account and you visit another web, you won’t like that that page makes your browser perform a request to your bank to transfer your money to somebody else’’s account. And you don’t want to be the programmer of that electronic banking app ;).

How to prevent it? One way is CSRF tokens. This is the solution that Symfony employs.

What is a CSRF token?

A CSRF token is a key generated by the server that depends on a secret key, a session id and the class of the form. You provide it with a form as a hidden value, and you expect the browser to send that key back to you when the form is submitted. As the attacker doesn’t know your secret key, he cannot generate a valid token. Simple, uh?

Let’s take a look at the relevant pieces of code of sfForm:

$CSRFProtection    = false,
$CSRFSecret        = null,
$CSRFFieldName     = '_csrf_token',

The first variable determines whether the CSRF protection is activated. CSRFSecret holds the secret key of theapplication, and CSRFFieldName is… the name of the field in the form.

/**
* Returns a CSRF token, given a secret.
*
* If you want to change the algorithm used to compute the token, you
* can override this method.
*
* @param string $secret The secret string to use (null to use the current secret)
*
* @return string A token string
*/
public function getCSRFToken($secret = null)
{
  if (is_null($secret))
  {
    $secret = self::$CSRFSecret;
  }

  return md5($secret.session_id().get_class($this));
}

See? it is doing an MD5 encryption with your session_id, your secret and the name of the class of the form. The session is important, because if the form is left open enough time to let the session expire, the validation will fail.

/**
* Adds CSRF protection to the current form.
*
* @param string $secret The secret to use to compute the CSRF token
*/
public function addCSRFProtection($secret)
{
  if (false === $secret || (is_null($secret) && !self::$CSRFProtection))
  {
    return;
  }

  if (is_null($secret))
  {
    if (is_null(self::$CSRFSecret))
    {
      self::$CSRFSecret = md5(__FILE__.php_uname());
    }

 $secret = self::$CSRFSecret;
 }

 $token = $this->getCSRFToken($secret);

 $this->validatorSchema[self::$CSRFFieldName] = new sfValidatorCSRFToken(array('token' => $token));
 $this->widgetSchema[self::$CSRFFieldName] = new sfWidgetFormInputHidden();
 $this->setDefault(self::$CSRFFieldName, $token);
}

This function sets a secret key if there is not $secret parameter depending on the uname of the machine, but the interesting thing here is that the validator for the field is set to sfValidatorCSRFToken:

class sfValidatorCSRFToken extends sfValidatorBase
{
  /**
  * @see sfValidatorBase
  */
  protected function configure($options = array(), $messages = array())
  {
    $this->addRequiredOption('token');

    $this->setOption('required', true);

    $this->addMessage('csrf_attack', 'CSRF attack detected.');
  }

  /**
  * @see sfValidatorBase
  */
  protected function doClean($value)
  {
    if ($value != $this->getOption('token'))
    {
      throw new sfValidatorError($this, 'csrf_attack');
    }

  return $value;
  }
}