CSRF (cross-site request forgery) заставляет браузер жертвы отправить запрос на ваш сайт от её имени. Атакующий не видит ответа, но запрос выполнится с cookie жертвы: перевод денег, смена email, удаление данных.
Как это работает
Жертва залогинена на bank.ru, cookie сессии в браузере. Она открывает страницу атакующего, на которой:
<form action="https://bank.ru/transfer" method="POST">
<input name="to" value="attacker"><input name="amount" value="10000">
</form>
<script>document.forms[0].submit()</script>
Браузер исторически прикладывал cookie bank.ru к любому запросу на bank.ru, откуда бы он ни шёл. Сервер видит валидную сессию и выполняет перевод.
Условия CSRF: аутентификация через то, что браузер прикладывает автоматически (cookie, HTTP Basic, клиентские сертификаты), и действие, которое можно вызвать предсказуемым запросом.
Защита
1. SameSite у cookie. Атрибут говорит браузеру, когда прикладывать cookie к межсайтовым запросам:
Strict: никогда. Надёжно, но пользователь, пришедший по ссылке из почты, окажется не залогинен.Lax: только при навигации верхнего уровня безопасными методами (переход по ссылке GET). POST-форма с чужого сайта cookie не получит. Chrome и другие Chromium-браузеры используют Lax по умолчанию для cookie без атрибута.None: всегда, только вместе сSecure. Нужен для встраиваемых виджетов и межсайтовых сценариев.
SameSite работает на уровне сайта (регистрируемый домен), а не origin: evil.example.com и app.example.com один сайт. Поддомен под контролем атакующего обходит SameSite.
2. CSRF-токен. Случайное значение, привязанное к сессии, кладётся в форму или заголовок и проверяется сервером. Сайт атакующего его не знает. Варианты: synchronizer token (хранится в сессии) и подписанный double submit cookie (токен в cookie и в заголовке, подпись привязывает к сессии). Сравнивать токены нужно за постоянное время.
3. Проверка Origin и Fetch Metadata. Браузер добавляет заголовок Origin к POST-запросам и Sec-Fetch-Site (same-origin, same-site, cross-site, none) к запросам в современных браузерах. Сервер может отклонять изменяющие запросы с чужим источником. Это хорошая дополнительная мера и основная для API.
4. Не менять состояние в GET. GET, HEAD, OPTIONS должны быть безопасными. Иначе <img src="/delete?id=1"> удалит данные, и SameSite Lax не поможет.
5. Повторное подтверждение для критичных действий: пароль, код из MFA.
Фреймворки (Django, Rails, Laravel, Spring Security) включают CSRF-защиту по умолчанию. Уязвимость часто появляется, когда её отключают («мешала тестам») или делают @csrf_exempt на эндпоинте.
Токены в заголовке
Если API аутентифицируется токеном в заголовке Authorization, который выставляет JavaScript, классический CSRF невозможен: браузер не добавит заголовок сам. Зато токен в localStorage доступен любому XSS. Выбор хранения это компромисс: cookie с HttpOnly и SameSite плюс защита от CSRF, или заголовок и строгая защита от XSS.
CORS не защищает от CSRF
CORS ослабляет политику одного источника (same-origin policy): разрешает чужим сайтам читать ответы. Он не мешает отправить запрос. Простые запросы (форма POST с application/x-www-form-urlencoded) уходят без preflight, и сервер их выполнит при любых настройках CORS.
Опасные ошибки CORS:
- Отражать любой
OriginвAccess-Control-Allow-Originвместе сAccess-Control-Allow-Credentials: true: любой сайт читает данные пользователя. - Разрешать
nullorigin (его отправляют песочницы и локальные файлы). - Проверять origin через
endsWith("example.com"): пройдётevilexample.com.
Звёздочку * браузер не разрешает вместе с credentials.
Частые ошибки
- «У нас SPA и JSON, CSRF не страшен», а сервер принимает и формы.
- Отключённая CSRF-защита фреймворка.
- Изменяющие действия через GET.
- CORS со всеми origin и credentials.
Что спросят на собесе
- Как работает CSRF и какие условия нужны атакующему.
- Чем Strict отличается от Lax.
- Защищает ли CORS от CSRF.
- Нужна ли защита от CSRF, если токен в заголовке Authorization.
- Как правильно проверять Origin.