Оказывается в интернете существует много потенциальных способов навредить, или частично манипулировать пользователем без его ведома. Одним из таких старинных способов причинить вред — стала уязвимость под названием Сross Site Request Forgery — межсайтовая подделка запросов.
В двух словах — уязвимость позволяет заставить пользователя выполнить какой-либо запрос на произвольном ресурсе, если пользователь там авторизован. С одной стороны — это и не уязвимость вовсе, т. к. позволяет проделать действия, на которые у пользователя и так есть права, с другой стороны — это манипуляция от имени чужого пользователя.
Уязвимость настолько серьезная, что во фреймворке Symfony 2 при установке — ей выделили отдельную страницу конфигурации
Все остальные высокоуровневые фреймворки тоже не прочь похвастаться на борту встроенной защитой CSRF. Давайте просто разберем суть проблемы
А суть заключается в том, что каким-то образом посетитель не видя формы уже отправляет нам готовый запрос на то или иное действие. Разве такое может быть?
Теоретически может. Пользователь мог открыть форму еще в прошлом году (в прошлой сессии) и не обновлять страницу, а нам покажется, что он вот так пришел из неоткуда. Определять пользователя по исходной ссылке $_SERVER['HTTP_REFERRER']
— тоже было бы не правильно. Некоторые браузеры ее не передают.
Мы можем давать каждому пользователю ключик, который будет соответствовать его сессии и добавлять этот ключик в каждую форму. Таким образом при каждой отправке — мы сможем проверить ключик на соответствие и понять — сам ли человек сделал действие после того, как форму увидел, или это действие пытается сделать кто-то другой без ведома основного посетителя.
Именно так и поступают вышеуказанные фреймворки. Вот пример из Symfony 2
<?php
public function generateCsrfToken($intention){
return sha1($this->secret.$intention.$this->getSessionId());
}
Чем плох данный подход?
Хотя бы тем, что ключ будет зависеть от параметра сессии. А значит, что если сессия пользователя измениться — он сможет отправить форму только увидев ее еще раз. Представьте что у вас большая форма и на заполнение пользователю нужно время. По окончанию заполнения — сессия теряется. Даже если по кукам — пользователь автоматически авторизуется — защита не пройдет, сессия изменится.
Этот подход хорош для защиты, если пользователь не авторизован. Но когда пользователь не авторизован — защита как таковая не нужна. Все, что мы можем получить — это передачу с новых IP, что актуально только для накрутки анонимных голосовалок.
А если пользователь авторизован — этот подход избыточен. Достаточно привязаться к уникальному идентификатору пользователя, будь то ID
, или LOGIN
. Пользователю будет выдаваться вечный ключ для его уникального логина. Злоумышленник, не зная ключа, не сможет правильно повторить форму для подачи запроса. На этом история заканчивается. В случае повторной авторизации — ключ будет восстановлен и форма примется без проблем.
Если есть опаска, что ключ может быть каким-то образом получен — выдавайте ключ на время, например так
<?php
function generateCSRF(){
return sha1($this->secret.$_SESSION['user']['id'].date('my'));
}
Выдаем ключ на месяц для конкретного пользователя. Случай, что пользователь начнет заполнять форму вечером в последний день месяца, а отправит ее на следующие сутки — крайне редок. Мы же получаем надежную защиту, которая автоматически меняется каждый месяц. Нужно чаще — используйте другие форматы даты — d
– день, W
– неделя
Таким образом — защита работает универсально для пользователя нужное количество времени
Теперь давайте разберем, как сделать, чтобы защита CSRF работала всегда для всего сайта автоматически без напряга для разработчиков (не нужно при создании каждой формы о ней думать) и без напряга для сервера (не создает нагрузки)
Чтобы генерация защитного ключа не создавала нагрузки — достаточно определять ключ всего раз при каждой авторизации пользователя и хранить где-то, скажем в параметрах сессии.
Например так:
<?php
function login($login,$pass){
…
$_SESSION['user']=$user;
$_SESSION['csrf']=sha1($this->secret.$_SESSION['user']['id'].date('my'));
}
А чтобы разработчикам нагрузки она не создавала — достаточно автоматически добавлять наш параметр в формы после загрузки страницы. Как? С помощью Javascript
Добавим наш параметр в главный шаблон
<script>var csrf='<?=$_SESSION['csrf']?>';</script>
И отдельный скрипт, который отработает по окончанию загрузки страницы
$(function(){
if (typeof(csrf)=='string'){
$('<input type="hidden" name="csrf" value="'+csrf+'">').appendTo('form[method="post"]');
}
})
Все. Мы автоматически добавили наш параметр во все формы с методом POST на странице. Если у нас есть какие-либо Ajax POST запросы — тут уже добавлять придется вручную
$.post(page,{csrf: csrf,...});
Все что остается — поставить на главную страницу — форму проверки
<?php
if ($_SESSION['csrf']){
if ($_SERVER['REQUEST_METHOD']=='POST'){
if ($_POST['csrf']!=$_SESSION['csrf'])die('CSRF защита сработала');
}
}
Вместо die
– можно добавить ошибку, чтобы вывести ее и вернуть заполненную форму в ответ
Поздравляю. Защита CSRF универсально легко побеждена.
Теперь на вашем сайте никто не удалит свою анкету без своего ведома, а также никто не оставит комментарии и не запостит пост с участием чужого сайта