27 марта 2012 г.

Zend, AJAX и защита от CSRF

Очень многие не заботятся о защите форм на своих проектах от CSRF-атак, а зря - я, например, через эту уязвимость карму кое где накручиваю (тссс!). А между тем защититься проще некуда.

Если это обычная форма, то в Zend-е есть чудесный элемент Zend_Form_Element_Hash. Просто добавляете его в формы и дишите ровно.
Однако, если у вас много интерактивных форм работающих на AJAX-е, то придётся написать свой небольшой велосипедик. Свой вариант я и представлю.
Самая надёжная защита от CSRF строится на токенах - уникальных ключах, генерируемых при каждом запросе формы. Рассказывать о них я не буду (кому надо - вот пост на хабре). Скажу только, что для себя я выбрал многоразовые токены, живущие в течении сессии юзера.

Для начала стоит отметить, что все AJAX запросы стоит чекать на предмет корректного реферера, одно это неплохо закроет вас от CSRF. Однако, такие гиганты как, например, stackoverflow.com, реферером не ограничиваются и вставляют ещё и токен. Сделаем так и мы.

Итак, как уже упоминалось, я выбрал многоразовые токены, единые для каждого юзера. Это позволит локализовать атаку временем жизни сессии одного юзера, к тому же таким, который как либо сольёт свой токен злоумышленнику. Мне таких не жалко, в общем то =)

Для начала токен надо сгенерить и запомнить. У меня это происходит при входе в систему и выглядит примерно так:
Copy Source | Copy HTML
$res = $adapter->getResultRowObject();
$user = new stdClass();
$user->id = $res->id;
$user->login = $res->login;
$user->role = $res->role;    
$user->csrf = hash('sha256', uniqid( mt_rand(), true ));
Zend_Auth::getInstance()->getStorage()->write($user);
В данном коде $adapter - используемый вами экземпляр Zend_Auth_Adapter_* (в моём случае Zend_Auth_Adapter_DbTable).

Следом необходимо определиться каким образом наши AJAX вызовы будут получать этот токен. Для удобства я сделал хелпер вида, печатающий скрытый input со значением нашего токена. Вызывая этот хелпер на страницах, на которых присутствуют важные интерактивные формы, мы получаем возможность по единому селектору добраться до значения токена.
Copy Source | Copy HTML
class App_Helper_PrintCsrfToken extends Zend_View_Helper_Abstract
{
    public function printCsrfToken()
    {       
        $user = Zend_Auth::getInstance()->getStorage()->read();
        if(!is_null($user))
            printf('<input id="js-csrf-token" type="hidden" class="hide" name="token" value="%s"/>', $user->csrf);       
    }
}
Тут, кстати, надо упомянуть основной плюс многоразового токена: если действий (читай AJAX форм) на странице много, то не приходится после каждого обновлять все токены. На моей практике это привело к сильному упрощению логики вызовов, что есть добро.
Есть тут и другое преимущество - пользователь может спокойно работать одновременно в нескольких вкладках, не ломая действиями в одной вкладке сохранённые токены в других.

Вызываем хелпер на странице с формой (или просто в шаблоне, если лень)
Copy Source | Copy HTML
<?php $this->printCsrfToken(); ?>

Добавляем в вызов AJAX дополнительное поле
Copy Source | Copy HTML
jQuery.post('/ajax/test/',{                
   'csrf': jQuery('#js-csrf-token').val(),
   ....
},function(data){...},'json');

Собственно на этом велосипед и заканчивается. При каждом новом входе пользователя в систему будет сгенерирован уникальный ключик, который с помощью хелпера вида и единого селектора можно внедрить во все свои AJAX-формы. Осталось только проверить токен на стороне сервера и залогировать нарушителя. Для этого у мну тоже образовался хелпер действий:
Copy Source | Copy HTML
class My_Action_Helper_TokenCheck extends Zend_Controller_Action_Helper_Abstract
{   
    public function direct($token)
    {  
        $user = Zend_Auth::getInstance()->getStorage()->read();
        return ( !is_null($user) && $user->csrf === $token );
    }
}

Засим всё. Помните о CSRF, берегите пользователей.

Комментариев нет: