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
Атрибут 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-защиту: скрипт на самом сайте может прочитать токен. Поэтому эти защиты дополняют, а не заменяют друг друга.
Рекомендуемый набор
SameSite=Lax(илиStrict) иSecure,HttpOnlyдля сессионных cookie.- Встроенная CSRF-защита фреймворка для всех изменяющих запросов.
- Никаких изменений состояния через GET.
- Проверка
OriginилиSec-Fetch-Siteкак дополнительный слой. - Повторный ввод пароля или второй фактор для критичных действий — смены почты, пароля, вывода денег.