Разбираемся с CSRF. Исполнительность браузера на службе зла

Безопасность

Так ли безопасно бездумно переходить по ссылкам, которые присылают нам знакомые (и не очень) люди в интернете? Если вы ответили «да», то спешу вас разочаровать — это не совсем так. Бывают случаи, когда при переходе по ссылке браузер фоном выполняет какое-то не запрошенное лично вами действие от вашего имени на другом сайте. Это и есть CSRF — Cross Site Request Forgery.

И хорошо, если это что-то безобидное вроде лайка на посте или подписки на рассылку. Но сам факт, что злоумышленник может тайно действовать от вашего лица, должен пробудить некоторую бдительность.

В статье про CORS мы вскользь упомянули этот вид атаки, а сегодня разберемся с ней в деталях

Как выглядит CSRF атака. Разбираем пошагово

Допустим, мы зашли в уже знакомый нам по статье про CORS вымышленный интернет-банк bank.ru и авторизовались с целью оплатить коммунальные платежи. В этот момент в браузере поселилась сессионная кука — пропуск, который подтверждает: «Это владелец счета, он может совершать с ним и его содержимым любые действия».

Спустя 5 минут работы с платёжками, мы устали от этой неприятной обязанности и открыли новую вкладку, чтобы отвлечься и расслабиться. По присланной в почту ссылке мы перешли на сайт kotyki.ru. Но администратор kotyki.ru хочет получить доступ к нашим деньгам. Зная о «дырявости» API нашего банка, он добавил на страницу такой код:

<img src="https://bank.ru/transfer?to=hacker&amount=1000" width="1" height="1">

Картинка размером в 1 пиксель, совсем невидимая для человека. Но браузер, увидев тег <img>, автоматически пытается загрузить содержимое атрибута src. Он отправляет GET-запрос на bank.ru/transfer?to=hacker&amount=1000. При этом автоматически к запросу прикрепляются ваши сессионные куки (потому что они принадлежат тому домену, на который вы отправляете запрос).

Сервер банка получает запрос. Видит: «О, куки. Они от настоящего владельца счета. Значит, сам владелец хочет перевести 1000 рублей хакеру. Кто я такой, чтобы ему мешать». И выполняет перевод.

Почему не спасает Same‑Origin Policy?

Вы можете спросить: «Но как же политика одинакового источника? Она же должна блокировать запросы с чужого сайта!»

Вот тут главная загвоздка. Same‑Origin Policy (SOP) блокирует чтение ответа, но не отправку запроса!

  • Браузер позволяет отправить POST/GET‑запрос с kotyki.ru на bank.ru.
  • Браузер прикрепляет куки от bank.ru, если они у него есть.
  • Браузер не показывает ответ от bank.ru сайту kotyki.ru (только тут и срабатывает SOP).

Но тому что совершает CSRF-атаку и не нужно читать ответ. Ему достаточно, чтобы запрос исполнился. Перевелись деньги — хорошо. Пользователь сменил пароль — отлично. Сообщение опубликовано — пусть и маленькая, но победа.

Как совершаются CSRF-атаки

Существует две линии атаки

1. GET-запросы с побочными эффектами

Мы разобрали именно такой пример. Наш условный bank.ru делает перевод через GET — GET /transfer?amount=1000&to=hacker. Например, любой тег <img> или <a href="..."> может дернуть эту ссылку. Хакеру достаточно заманить вас на страницу с такой ловушкой.

Как решается. При дизайне API никогда не используйте GET для изменений. Только POST, PUT, DELETE. Но и этого мало, потому что POST тоже можно подделать.

2. Подделка POST-запроса через скрытую форму

Злоумышленник создает на своем сайте невидимую форму и отправляет её автоматически через JavaScript

<form id="hack" action="https://bank.ru/transfer" method="POST">
    <input name="to" value="hacker">
    <input name="amount" value="1000">
</form>
<script>document.getElementById('hack').submit();</script>

Когда вы заходите на сайт, скрипт мгновенно отправляет форму и снова без вашего участия. Браузер снова послушно прикрепляет куки. Сервер думает, что это вы.

Как решается: Использовать CSRF-токены, о которых знает только ваш сайт (и которые не может угадать злоумышленник).

Как защититься? Три основных метода

Метод 1: CSRF-токены

Сервер при каждой генерации страницы добавляет в форму секретный случайный токен.

<form method="POST" action="/transfer">
    <input type="hidden" name="csrf_token" value="a7f3d9e2...">
    <input name="amount">
    <button>Перевести</button>
</form>

При отправке формы сервер проверяет в том числе и совпадение токенов. У зловредного админа kotiky.ru нет возможности узнать этот токен. Поэтому его подделанная форма не пройдет.

Плюсы: Надежно.
Минусы: Требует хранить состояние на сервере.

Метод 2: SameSite cookies

Браузеры теперь поддерживают атрибут кук SameSite. Вы говорите браузеру:

Set-Cookie: session_id=abc123; SameSite=Strict
  • SameSite=Lax — предъяви куки если я действительно перехожу по ссылке или инициирую действие (но не при отправке формы из iframe или скрипта)
  • SameSite=Strict — если запрос приходит с другого сайта не предъявляй куки вообще никогда

То есть если банк поставит SameSite=Strict, то при заходе на kotyki.ru браузер не прикрепит куки банка к поддельному запросу. Атака не сработает.

Плюсы: Не требует изменять код форм.
Минусы: Старые браузеры не поддерживают (но сейчас уже все современные поддерживают).

Метод 3: Проверка Referer / Origin

Сервер смотрит на заголовок Referer или Origin. Откуда пришел запрос? Если Referer — kotyki.ru, а должен быть только bank.ru — сервер отклоняет запрос.

if (req.headers.origin !== 'https://very-bank.com') {
    return res.status(403).send('CSRF атака!');
}

Плюсы: Просто.
Минусы: Referer может отсутствовать (например, при переходе из HTTPS в HTTP или из-за политик конфиденциальности).

А разве CORS недостаточно?

Легко перепутать CORS и CSRF. Четыре буквы, начинаются с C — очень похоже. Но различать их стоит и запомнить различия можно так:

  • CORS защищает данные. Он не дает чужому сайту прочитать ответ от вашего сервера.
  • CSRF защищает действия. Он не дает чужому сайту выполнить опасный запрос от вашего имени.

У нас может быть строжайше настроенный CORS (Access-Control-Allow-Origin: только мой сайт), и все равно наши пользователи будут уязвимы для CSRF, потому что запрос приходит с их IP, с их куками, а сервер не знает, что его инициировал злой админ kotiky.ru.

Итог: что важно запомнить

  • CSRF — атака, при которой вредоносный сайт заставляет браузер выполнить запрос на сайт, где вы залогинены.
  • Уязвимость существует, потому что браузер автоматически прикрепляет куки к запросам на любой сайт, даже если запрос пришел с чужой страницы.
  • Спасают CSRF-токены (спрятанные в формах) или SameSite cookies (атрибут кук).
  • CORS не защищает от CSRF. CORS — про чтение ответа. CSRF — про выполнение действия.
  • Любое действие, которое что-то меняет (деньги, пароль, подписка, удаление), должно требовать либо CSRF-токен, либо повторную аутентификацию (пароль/капчу).

Чтобы проверить, уязвимы ли вы, можно попробовать отправить форму из HTML-файла на локальной машине. Если сработало — бросайте читать непонятные блоги и бегите чинить защиту.

Желаю успехов!

Симо Мофин
Симо Мофин

Senior Frontend Developer
Главный по блогу