Эта уязвимость (XSRF, CSRF, Cross-Site Request Forge - межсайтовая подделка запросов) позволяет злоумышленнику, заманив пользователя на свою страницу, отправлять запросы от его имени. Чтобы понять то, что ниже написано, тебе надо знать, как делаются HTML-формы и как они обрабатываются на стороне PHP (что такое $_POST
и $_GET
).
Допустим, у тебя на сайте (bank.example.com
) есть какая-то форма, доступная только залогиненным пользователям. Допустим, ты делаешь сайт банка и это форма перевода денег. Она может выглядеть примерно так:
<h1>Перевести деньги со счета</h1>
<form action="send.php" method="POST">
<label>Сумма: <input name="sum" value=""></label>
<label>Номер счета получателя: <input name="target" value=""></label>
<input type="submit" value="Отправить">
</form>
Скрипт-обработчик этой формы проверяет, залогинен ли пользователь, и если да и у него на счету есть нужная сумма, переводит ее на счет получателя.
Ничего подозрительного, верно? Неужели в такой простой форме из двух полей может быть уязвимость? А она есть.
Разработчик этой формы не учел, что форма может быть отправлена не только с сайта банка, но и с любого другого сайта. Злоумышленник может сделать на своем сайте (evil.example.com
) страницу с заполненной невидимой формой и яваскриптом (яваскрипт это язык, программы на котором встраиваются в HTML-страницу и выполняются в браузере), который отправляет эту форму при заходе на страницу:
<!-- Указываем в качестве адреса отправки сайт банка и делаем форму
невидимой, чтобы не пугать пользователя -->
<form action="http://bank.example.com/send.php" style="display: none;">
<input type="hidden" name="sum" value="9000">
<input type="hidden" name="target" value="счет злоумышленника">
</form>
<script>
// код на языке яваскрипт, автоматически отправляющий форму
document.forms[0].submit();
</script>
После этого злоумышленнику достаточно любым способом заманить твоего пользователя на свою страницу (например скинув ему ссылку, выложив ссылку на популярном сайте или купив размещение в рекламной сети, которая отобразит код злоумышленника с формой на большом числе партнерских сайтов за плату). Если пользователь перейдет по ссылке (или просто зайдет на один из сайтов, участвующих в рекламной сети), сработает яваскрипт на странице и отправит форму на сайт банка от имени пользователя. Если пользователь был в этот момент залогинен, то форма будет обработана и деньги переведены. Разумеется, злоумышленник может предусмотреть отправку формы в цикле, до тех пор, пока счет не будет опустошен.
Обрати внимание на вариант с рекламной сетью - с ее помощью злоумышленник может вставить свою страницу как рекламу на большом числе сайтов, подключенных к рекламной сети, и ему не надо специально заманивать пользователей на какую-то страницу.
Конечно, в наше время банки, наученные горьким опытом, требуют подтверждения через другие каналы вроде SMS при переводе денег. Но XSRF можно использовать на любых сайтах, где есть формы. Например, с ее помощью можно рассылать от имени пользователя сообщения в соцсети.
XSRF может использоваться не только с формами. Допустим, на некотором форуме переход по ссылке http://forum.example.com/logout.php
разлогинивает пользователя. Допустим, на этом форуме в текст сообщения можно вставлять картинки с любым URL. Злоумышленник может запостить «картинку» с URL, ведущим на ссылку разлогинивания, и когда пользователь зайдет на страницу с такой картинкой, его браузер отправит запрос и автоматически разлогинит его.
Атака XSRF может применяться и на сайтах, где нет авторизации пользователей. Допустим, у нас есть сайт, где разыгрывается приз и победитель выбирается голосованием. Голосовать может любой пользователь, без регистрации, но только 1 раз с 1 IP-адреса. Если форма голосования не защищена от CSRF, то злоумышленник может сделать свою страницу с формой, заманивать на нее пользователей и тем самым накручивать голоса.
Надеюсь, ты понял, что это довольно серьезная уязвимость, и надо уметь от нее защищаться.
Чтобы бороться с отправленной без ведома пользователя формой, скрипт-обработчик на сервере должен отличить запрос, отправленный со своего сайта от запроса, отправленного с чужого. Правильный способ сделать это — выдать каждому пользователю индивидуальный код (токен), который хранится в куках, и вставить этот код в форму. А при проверке данных формы сравнить токены в куках и в $_POST
. Так как злоумышленник не знает токена пользователя, он не может вставить его в форму на своем сайте и пройти проверку.
Вот примерный алгоритм. Прежде всего, мы должны при заходе на страницу с формой выдать пользователю куку с токеном (а если она у него есть, продлить ей время жизни):
Если (у пользователя нет куки с токеном) {
$token = генерируем случайный код;
сохраняем код в куки пользователю;
} иначе {
$token = код из куки;
Продлеваем время жизни куки;
}
Код должен быть сложным, чтобы злоумышленник не мог его угадать. 32 символа [a-zA-Z0-9] дают около 6232 = 2×1057 комбинаций, что делает подбор нереальным.
Время жизни можно ставить от нескольких часов до нескольких дней. Если пользователь не заходит на сайт в течение этого времени, то кука удалится и позже он получит новый токен. Не стоит ставить время слишком маленьким, так как в этом случае у долго заполняющего форму пользователя кука может удалиться и форма не будет принята.
Далее, мы должны при выводе формы добавить скрытое поле с токеном в форму:
<input type="hidden" name="token" value="<?= htmlspecialchars($token, ENT_QUOTES) ?>">
Ну и наконец при обработке данных формы мы должны сравнить токен в куках и в данных формы:
Если (токен в куках пуст или токен в форме пуст или они не равны) {
добавляем сообщение об ошибке и не принимаем данные;
}
В случае ошибки токена можно просто вывести форму с введенными данными и вывести сообщение с просьбой проверить введенные данные и отправить форму повторно, если они верны.
Точно так же с помощью токена можно защитить и действия, выполняемые по ссылке (хотя по идее для таких вещей должны использоваться формы). Например, ссылка на страницу разлогинивания с сайта с токеном может выглядеть как /logout.php?token=abcdef123456
(именно так выглядят ссылки на многих сайтах).
У кук есть флаг httpOnly
, который запрещает доступ к ним из яваскрипта. Если его включить для куки с токеном, защита будет более надежной.
Если ты используешь фреймворк (вроде Yii или Symfony 2), возможно в его реализации классов для обработки форм есть защита от XSRF. Но ты должен внимательно проверить документацию и HTML код — может быть, защита по умолчанию отключена или требует отдельной настройки.
Ты можешь увидеть где-то неправильные советы, например:
- проверять заголовок
Referer
. Это не так надежно, так как некоторые браузеры и прокси могут не передавать этот заголовок, а также, есть способы отправлять форму без него. - генерировать токен не случайно, а из IP-адреса пользователя. Это может приводить к багам, например если у мобильного пользователя в процессе заполнения формы разорвалось соединение (и телефон переподключился, получив другой IP) то при отправке формы токен не совпадет.
В новых браузерах при каждом запросе отправляется заголовок Origin
, который говорит о том, с какой страницы отправлена форма. Возможно, в будущем его можно будет использовать вместо токенов.
Способ, описанный выше, подразумевает хранение токена в 2 местах: в форме в виде скрытого поля и в куках. Если у пользователя по какой-то причине отключены куки, то он не сможет пройти проверку. В принципе, делать с этим ничего не надо - без кук и так большинство форм и авторизация работать не будет. Но что, если нас одолел приступ перфекционизма и мы хотим сделать, чтобы защита от XSRF работала бы без поддержки кук?
Для этого нам понадобятся так называемые nonce (уникальные не повторяющиеся коды) и хранилище для них, например база данных или redis. При отображении формы мы генерируем уникальный nonce, вписываем в скрытое поле и сохраняем его в хранилище, записав заодно время генерации. При получении мы берем токен из формы и проверяем, есть ли такой в хранилище? Если да, то все в порядке, если нет - нам пытаются обмануть.
Злоумышленник не имеет доступа к нашей базе токенов и не может подставить в форму правильный токен. Это по сути тот же подход что и выше, просто мы храним вторую копию токена не в куках у пользователя, а на сервере.
Разумеется, после успешного использования мы удаляем nonce из хранилища, чтобы им нельзя было воспользоваться второй раз (хотя я и не очень представляю как это можно сделать). Также, чтобы хранилище не разрасталось, мы должны по расписанию удалять из него все токены старше определенной даты, например старше 1 суток.
Этот способ работает без кук, но он намного сложнее в реализации, требует хранилища на сервере и его очистки по расписанию. Наверно, в большинстве случаев так далеко заходить не требуется.
Зайди на свои любимые сайты (где есть авторизация), открой инструменты разработчика в браузере (Ctrl + Shift + I), изучи формы и ссылки на сайте, свои куки, и попробуй понять, есть на них защита от XSRF или нет.
- http://habrahabr.ru/post/235247/ - ошибки при реализации защиты от CSRF
- http://habrahabr.ru/post/21626/ - CSRF на вконтакте в 2008 (тогда у них много уязвимостей было)
- http://intsystem.org/812/stripping-referer-in-redirect/ - некоторые варианты обойти проверку через Referer (потому ты должен использовать токены)