18
Янв

Некоторые особенности CSRF Protection в symfony

В данной заметке я привожу некоторую ситуацию, с которой я столкнулся при работе с формами в замечательном PHP фреймворке Symfony. В частности, проблема возникала при включенной защите от межсайтовых запросов (CSRF Protection), что это за защита вы можете прочитать в википедии (http://ru.wikipedia.org/wiki/CSRF, http://www.inattack.ru/article/552.html).

В симфонии при включенной CSRF защите в форму подставляется скрытое поле с именем _csrf_token, его значение формируется как md5 хеш от секретной строки, имени класса и идентификатора сессии (session_id).
Пример формирования значения токена в Symfony:

  // sfForm.class.php
  public function getCSRFToken($secret = null)
  {
    ....
    return md5($secret.session_id().get_class($this));
  }

Следовательно, если после некоторого действия, значение возвращаемое session_id() меняется, то дальнейшая валидация созданных до этого момента форм, не будет корректно обрабатываться.
Такие случаи могут возникать, например, при авторизации пользователя (sfGuardPlugin) и дальнейшей обработке форм в одном запросе.
Пример, у нас есть две формы c полями:
1. sfGuardFormSignin: signin[username], signin[password]
2. AddressForm: address[phone],address[city],… -

Мы хотим в одном запросе авторизовать пользователя с помощью логина пароля и сохранить обязательные поля из формы address
Делаем примерно так:

$this->auth_form = !$this->getUser()->isAuthenticated() ? new sfGuardFormSignin() : null;
$this->address_form = new AddressForm();
if ($request->isMethod('post'))
{
  // авторизуемся
  if (!$this->getUser()->isAuthenticated())
  {
    $this->auth_form->bind($request->getParameter('signin'));
    if ($this->auth_form->isValid())
    {
      $values = $this->auth_form->getValues();

      $this->getUser()->signin($values['user'], array_key_exists('remember', $values) ? $values['remember'] : false);
      $this->auth_form=null;
    }
  }

  // обрабатываем форму адреса
  $this->address_form->bind($request->getParameter('address'));
  if ($this->address_form->isValid())
  {
    // что-то делаем с формой, например сохраняем
    $this->address_form->save();
    //...

    $this->redirect('@somepath');
  }

}

Допустим, авторизация прошла успешно, но валидация формы адреса не прошла. Тогда нам покажется форма адреса с ошибками, но исправив ошибки мы все равно получим не валидную форму так как session_id изменился, а форма адреса создавалась с учетом старого его значения и
нам будет в любом случае выдавать ошибку «csrf token: Required.».
Как избежать подобного?
Способ который я применил (на мой взгляд не очень красивый) заключается в следующем: нужно после авторизации поставить в сессию атрибут
о временном отключении «CSRF Protetion» перед обработкой форм проверять данный атрибут и отключать защиту CSRFT.
Пример:

$this->auth_form = !$this->getUser()->isAuthenticated() ? new sfGuardFormSignin() : null;
$this->address_form = new AddressForm();
if ($request->isMethod('post'))
{

  // Авторизация
  if (!$this->getUser()->isAuthenticated())
  {
    $this->auth_form->bind($request->getParameter('signin'));
    if ($this->auth_form->isValid())
    {
      $values = $this->auth_form->getValues();

      $this->getUser()->signin($values['user'], array_key_exists('remember', $values) ? $values['remember'] : false); // <<< здесь меняется session_id
      $this->getUser()->setAttribute('disable_csrf',true); // <<< снимаем защиту CSRF
      $this->auth_form=null;
    }
  }

  // Отключаем защиту если требуется
  if ($this->getUser()->getAttribute('disable_csrf',false))
  {
    sfForm::disableCSRFProtection();
    unset ($this->address_form[sfForm::getCSRFFieldName()]);
  }

  // обработка формы адреса
  $this->address_form->bind($request->getParameter('address'));

  if ($this->address_form->isValid())
  {

    // что-то делаем с формой, например сохраняем
    $this->address_form->save();
    //...

    $this->getUser()->setAttribute('disable_csrf',null); // если все хорошо включаем защиту CSRF обратно
    $this->redirect('@somepath');
  }

}

Все вышеприведенное тестировалось на версии symfony 1.2, в этой версии CSRF защиту можно отключить только глобально. В более новых версиях фреймворка (1.3, 1.4), появилась возможность отключать защиту локально для конкретной формы по отдельности, что более правильно.
З.Ы. Если кто-то скажет, как подобное можно более красиво решить, буду очень благодарен :)