Skip to content

Latest commit

 

History

History
executable file
·
118 lines (77 loc) · 16.3 KB

xsrf.md

File metadata and controls

executable file
·
118 lines (77 loc) · 16.3 KB

Уязвимость XSRF

Эта уязвимость (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 или нет.

Ссылки