CSRF

Материал из llmdocs — справочник разработчика
CSRF
Полное названиеCross-Site Request Forgery
КлассификацияCWE-352
Условие атакиаутентификация по cookie, которые браузер прикладывает автоматически
Последствиясмена пароля и почты, переводы, изменение настроек от имени жертвы
ЗащитаSameSite cookie, синхронизирующий токен, проверка Origin, Fetch Metadata
Не защищаетCORS сам по себе, секретная cookie, POST вместо GET

CSRF (Cross-Site Request Forgery, межсайтовая подделка запроса) — атака, при которой сайт злоумышленника заставляет браузер жертвы отправить запрос на другой сайт, где жертва уже вошла в систему. Браузер автоматически прикладывает к запросу cookie целевого сайта, и сервер воспринимает его как действие самого пользователя: смену пароля или почты, перевод денег, удаление данных. В отличие от XSS, злоумышленник не читает ответ и не выполняет код на сайте — он лишь «нажимает кнопку» руками жертвы.

Как работает

Пользователь вошёл в интернет-банк, сессия хранится в cookie. Затем он открывает страницу злоумышленника, где есть скрытая форма:

<form action="https://bank.example/transfer" method="POST" id="f">
  <input type="hidden" name="to" value="attacker">
  <input type="hidden" name="amount" value="50000">
</form>
<script>document.getElementById("f").submit()</script>

Браузер отправляет POST-запрос на bank.example вместе с сессионной cookie. Если сервер проверяет только cookie, перевод выполнится. Для GET-запросов достаточно даже <img src="https://site.example/delete?id=1">.[1]

Атака возможна, когда одновременно выполняются три условия: действие имеет ценность для злоумышленника, аутентификация основана только на том, что браузер прикладывает сам (cookie, HTTP Basic, клиентские сертификаты), и все параметры запроса злоумышленник может угадать.

Чем защищаться

Атрибут SameSite указывает браузеру, отправлять ли cookie при запросах с других сайтов:[3]

Значение Поведение
Strict cookie не отправляется ни при каких межсайтовых запросах, даже при переходе по ссылке
Lax отправляется только при навигации верхнего уровня методом GET (переход по ссылке)
None отправляется всегда; требует Secure
Set-Cookie: session=abc123; Secure; HttpOnly; SameSite=Lax; Path=/

Современные браузеры по умолчанию считают cookie без атрибута как Lax, что закрыло большинство классических атак. Но полагаться только на это не стоит: поведение по умолчанию различается между браузерами, «сайт» определяется по регистрируемому домену (поддомены одного сайта считаются своими), а GET-запросы, меняющие состояние, Lax не защищает.

CSRF-токены

Классическая защита — синхронизирующий токен: сервер генерирует случайное непредсказуемое значение, связанное с сессией, и вставляет его в каждую форму. При запросе, меняющем состояние, сервер сверяет токен; сайт злоумышленника не может его узнать.

<form method="POST" action="/settings/email">
  <input type="hidden" name="csrf_token" value="{{ csrf_token }}">
  ...
</form>

Для одностраничных приложений токен передают в заголовке (X-CSRF-Token). Вариант без хранения на сервере — double submit cookie: токен лежит и в cookie, и в заголовке, сервер сравнивает их; надёжнее подписанная версия, привязанная к сессии. Большинство фреймворков включают защиту из коробки: Django, Rails, Laravel, Spring Security; в Flask — расширение Flask-WTF.[5]

Проверка Origin и Fetch Metadata

Браузер добавляет к межсайтовым запросам заголовки, которые нельзя подделать из JavaScript:

  • Origin — с какого сайта отправлен запрос; сервер сравнивает его со списком разрешённых;
  • Sec-Fetch-Site: same-origin | same-site | cross-site | none — заголовки Fetch Metadata позволяют одной проверкой отклонить все межсайтовые запросы к изменяющим методам.[4]
if request.method not in ("GET", "HEAD", "OPTIONS"):
    if request.headers.get("Sec-Fetch-Site") not in (None, "same-origin", "none"):
        abort(403)

Частые заблуждения

  • «У нас POST, а не GET» — формы прекрасно отправляют POST.
  • «У нас настроен CORS» — CORS ограничивает чтение ответа JavaScript-кодом, но простые запросы (формы) всё равно доходят до сервера и выполняются.
  • «API принимает JSON» — запрос с Content-Type: application/json с чужого сайта требует preflight и обычно блокируется, но сервер, который принимает и text/plain или form-encoded, уязвим.
  • «Токен в cookie — секретный» — злоумышленнику не нужно знать cookie, их отправляет браузер.

Когда CSRF не страшен

Если API аутентифицирует запросы токеном в заголовке Authorization: Bearer ..., который JavaScript приложения добавляет явно (см. OAuth 2.0 и OpenID Connect), браузер не приложит его к чужому запросу, и классический CSRF невозможен. Однако хранение такого токена в localStorage повышает ущерб от XSS — компромисс между угрозами.

Кроме того, любая XSS-уязвимость обходит CSRF-защиту: скрипт на самом сайте может прочитать токен. Поэтому эти защиты дополняют, а не заменяют друг друга.

Рекомендуемый набор

  1. SameSite=Lax (или Strict) и Secure, HttpOnly для сессионных cookie.
  2. Встроенная CSRF-защита фреймворка для всех изменяющих запросов.
  3. Никаких изменений состояния через GET.
  4. Проверка Origin или Sec-Fetch-Site как дополнительный слой.
  5. Повторный ввод пароля или второй фактор для критичных действий — смены почты, пароля, вывода денег.

См. также

Источники

  1. OWASP — Cross-Site Request Forgery Prevention Cheat Sheet
  2. CWE-352: Cross-Site Request Forgery (CSRF)
  3. MDN — SameSite cookies
  4. web.dev — Protect your resources from web attacks with Fetch Metadata
  5. Django Docs — Cross Site Request Forgery protection