CSRF: механика атаки и линии защиты
CSRF (Cross-Site Request Forgery) — атака, при которой злоумышленник заставляет браузер авторизованного пользователя выполнить нежелательный запрос к доверенному сайту. Ключевая особенность: атакующий не крадёт пароль и не ломает сервер — он использует нормальное поведение браузера, автоматически прикрепляющего cookies к запросам. Статья разбирает механику атаки, три линии защиты (SameSite, CSRF-токены, Origin), паттерн double submit cookie и специфику React, Vue, Next.js и Nuxt.
Содержание
- Что такое CSRF и почему он работает
- Классическая атака по шагам
- Почему CSRF не умер в эпоху SPA и JSON API
- SameSite cookies: защита на уровне браузера
- CSRF-токены: золотой стандарт
- Double Submit Cookie pattern
- Origin, Referer и дополнительные проверки
- CSRF в React, Vue, Next.js и Nuxt
- Когда CSRF-защита не нужна
- Практический чек-лист
- Терминология CSRF
- Ключевые тезисы для интервью
- Заключение
- Полезные ссылки
Что такое CSRF и почему он работает
CSRF (Cross-Site Request Forgery, подделка межсайтовых запросов) — это атака, при которой злоумышленник заставляет браузер авторизованного пользователя выполнить нежелательное действие на доверенном сайте.
Ключевое слово — браузер. Злоумышленник не крадёт пароль, не ломает сервер, не обходит CORS. Он использует нормальное поведение браузера: если у вас есть cookie для bank.com, браузер автоматически прикрепляет её к любому запросу на bank.com, независимо от того, кто инициировал запрос.
Аналогия: вы оставили доверенное письмо у двери курьеру с просьбой передать посылку. Злоумышленник подделывает такое же письмо и даёт его курьеру. Курьер не проверяет, от кого на самом деле письмо — он видит вашу подпись (cookie) и выполняет просьбу.
Классическая атака по шагам
- Пользователь входит в
bank.comи получает сессионную cookie. - Не выходя из системы, пользователь открывает
evil.comв другой вкладке. evil.comсодержит форму или JavaScript, который отправляет запрос наbank.com:
<!-- Форма автоматически отправляется при загрузке -->
<form action="https://bank.com/transfer" method="POST" style="display:none">
<input name="to" value="hacker_account" />
<input name="amount" value="10000" />
</form>
<script>document.forms[0].submit();</script>
- Браузер отправляет POST-запрос на
bank.comи автоматически прикрепляет cookie. - Сервер
bank.comвидит валидную сессию и выполняет перевод.
Важно: злоумышленник не видит ответ сервера из-за Same-Origin Policy. Но ему это и не нужно — важно, чтобы запрос дошёл и выполнился.
Почему CSRF не умер в эпоху SPA и JSON API
Частое заблуждение: «Мы используем React и fetch с JSON, формы — это прошлый век». Но CSRF работает и с JSON, если выполнены два условия:
- Аутентификация построена на cookies.
- Сервер принимает cross-origin запросы с нужным Content-Type.
JSON CSRF
fetch('https://bank.com/api/transfer', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ to: 'hacker', amount: 10000 }),
});
До появления SameSite=Lax браузер отправлял cookie и с таким запросом.
Простые Content-Type
Если сервер принимает text/plain или application/x-www-form-urlencoded, злоумышленник может отправить JSON-подобное тело через HTML-форму:
<form action="https://bank.com/api/transfer" method="POST" enctype="text/plain">
<input name='{"to":"hacker","amount":10000,"ignore":"' value='"}' />
</form>
Это создаёт валидный JSON, который некоторые серверы могут распарсить.
SameSite cookies: защита на уровне браузера
SameSite — атрибут cookie, который говорит браузеру, когда отправлять cookie при cross-origin запросах.
Значения
| Значение | Поведение |
|---|---|
Strict | Cookie отправляется только при запросах внутри того же сайта. Переход по ссылке из email не авторизует пользователя. |
Lax | Cookie отправляется при top-level навигации (GET-запросы по ссылке), но не при cross-site POST/iframe/fetch. |
None | Cookie отправляется всегда. Требует Secure. Используется для embedded-виджетов и SSO. |
С 2020 года Chrome и другие браузеры делают SameSite=Lax значением по умолчанию.
Пример
// Next.js — установка cookie
import { cookies } from 'next/headers';
cookies().set('session_id', token, {
httpOnly: true,
secure: true,
sameSite: 'strict',
path: '/',
});
// Express
res.cookie('session_id', token, {
httpOnly: true,
secure: true,
sameSite: 'strict',
});
Почему Lax — компромисс
Strict идеален с точки зрения безопасности, но может раздражать пользователей: если они перешли по ссылке из email, они будут выглядеть как неавторизованные. Lax защищает от POST-запросов CSRF, но не от GET-атак, где действие выполняется через URL.
Поэтому для критичных операций SameSite=Strict + CSRF-токен — лучшая комбинация.
CSRF-токены: золотой стандарт
Идея: сервер генерирует секретный токен, включает его в форму или страницу, а при мутации требует этот токен обратно. Злоумышленник не может узнать токен, потому что не может прочитать содержимое вашего сайта из-за Same-Origin Policy.
Классический flow
- Сервер генерирует токен и сохраняет его в сессии.
- Сервер вставляет токен в HTML-форму как скрытое поле.
- При отправке формы сервер сверяет токен из формы с токеном в сессии.
<form action="/transfer" method="POST">
<input type="hidden" name="csrf_token" value="abc123..." />
<!-- остальные поля -->
</form>
Для SPA и API
В SPA токен обычно отдаётся через отдельный endpoint или встраивается в HTML, а клиент отправляет его в заголовке:
// Сервер (Next.js)
export async function GET() {
const token = generateCsrfToken();
cookies().set('csrf_token', token, { httpOnly: true, sameSite: 'strict' });
return Response.json({ csrfToken: token });
}
// Клиент
fetch('/api/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken,
},
credentials: 'include',
body: JSON.stringify(data),
});
Сервер сверяет заголовок с cookie. Cookie httpOnly защищает от XSS, а заголовок защищает от CSRF.
Double Submit Cookie pattern
Альтернатива хранению токена в сессии: сервер устанавливает токен в cookie, а клиент отправляет его в заголовке или скрытом поле. Сервер проверяет, совпадают ли cookie и заголовок.
Set-Cookie: csrf_token=abc123; SameSite=Strict; Secure
X-CSRF-Token: abc123
Почему это работает: злоумышленник может заставить браузер отправить cookie (это и есть суть CSRF), но не может прочитать её и вставить в заголовок. Same-Origin Policy и HttpOnly мешают чтению.
Этот паттерн удобен для stateless API и микросервисов, но требует криптографически стойкого токена и защиты от подделки cookie.
Origin, Referer и дополнительные проверки
Сервер может проверять заголовки запроса:
Origin— домен, с которого пришёл запрос (отправляется при POST, PUT, DELETE).Referer— полный URL предыдущей страницы.
export async function POST(req: Request) {
const origin = req.headers.get('origin') || req.headers.get('referer');
const allowed = ['https://mysite.com'];
if (!origin || !allowed.some((o) => origin.startsWith(o))) {
return Response.json({ error: 'Invalid origin' }, { status: 403 });
}
}
Ограничения
Originможет отсутствовать в некоторых запросах.Refererможет быть заблокирован расширениями браузера или настройками приватности.- Эти проверки хороши как дополнительный слой, но не заменяют CSRF-токены.
CSRF в React, Vue, Next.js и Nuxt
React / Vue
Сами по себе фреймворки не защищают от CSRF — это задача серверной части. Но они влияют на то, как передаётся токен:
- В классических формах — скрытое поле
<input type="hidden" name="csrf_token" value={token} />. - В SPA — заголовок
X-CSRF-Tokenили кастомный header. - React 19
<form action={serverAction}>: фреймворк может автоматически встраивать защиту, если Server Action настроен корректно, но лучше явно проверять Origin и использовать CSRF-токены для критичных операций.
Next.js
next/headersпозволяет читать и устанавливать cookies в Server Components и Route Handlers.- Middleware может проверять CSRF-токены до выполнения запроса.
- Для Server Actions важно валидировать входные данные, авторизацию и Origin.
Nuxt
nuxt-securityмодуль может настраивать CORS, CSP и заголовки.- Server routes в Nitro получают доступ к cookies через
getCookie(event). - В Nuxt 3 для CSRF-защиты можно использовать
h3middleware или встроенные механизмы сессий.
Когда CSRF-защита не нужна
CSRF актуален только при cookie-based аутентификации. Если вы используете:
- Bearer tokens в заголовке
Authorization, которые фронтенд хранит в памяти; - JWT в
Authorization: Bearer, не cookie; - API-ключи, передаваемые явно;
то CSRF-атака невозможна, потому что браузер не добавляет эти токены автоматически.
Но: хранение токенов в памяти или localStorage создаёт другие риски (XSS, утечка через скрипты). Выбор между cookies + CSRF и Bearer + XSS — это классический компромисс безопасности.
Практический чек-лист
Базовая защита
- Установить
SameSite=LaxилиSameSite=Strictдля всех сессионных cookies. - Использовать
Secure(только HTTPS) иHttpOnlyгде возможно. - Добавить CSRF-токены для критичных операций: переводы, удаление, изменение email/пароля.
Для SPA
- Получать CSRF-токен через отдельный endpoint или встраивать в HTML.
- Отправлять токен в заголовке
X-CSRF-TokenилиX-XSRF-Token. - Проверять
Originна сервере.
Для Next.js / Nuxt
- Настроить cookies в middleware / server routes.
- Валидировать Server Actions и API routes.
- Использовать
nuxt-securityили аналогичные модули.
Антипаттерны
- Не полагайтесь только на CORS — CORS не защищает от CSRF.
- Не используйте GET-запросы для действий, изменяющих состояние.
- Не доверяйте заголовку
Refererв одиночку.
Терминология CSRF
| Термин | Значение |
|---|---|
| CSRF | Подделка межсайтовых запросов от имени авторизованного пользователя |
| SameSite | Атрибут cookie, контролирующий cross-origin отправку |
| CSRF-токен | Секретное значение, которое сервер проверяет при мутациях |
| Double Submit Cookie | Паттерн: токен одновременно в cookie и в заголовке/форме |
| Origin | Заголовок с доменом источника запроса |
| Referer | Заголовок с полным URL источника |
| HttpOnly | Cookie, недоступная из JavaScript |
| Secure cookie | Cookie, передаваемая только по HTTPS |
| Preflight request | OPTIONS-запрос перед CORS-запросом |
| SOP | Same-Origin Policy — политика одного источника |
Ключевые тезисы для интервью
- CSRF — атака на механизм аутентификации браузера: cookie автоматически прикрепляется к запросу, атакующий не видит ответ (SOP), но ему достаточно самого факта выполнения.
- CSRF работает и с JSON API:
credentials: 'include'в fetch,text/plainв форме или простой Content-Type обходят наивные защиты; CORS не защищает, так как контролирует чтение ответа, а не отправку. SameSite=Lax(по умолчанию с 2020) блокирует cross-site POST, но не GET по ссылке;Strictстроже, но неудобен для переходов из email.- CSRF-токены — золотой стандарт: сервер генерирует секрет, который атакующий не может прочитать из-за SOP; Double Submit Cookie — вариант для stateless API, где токен в cookie и заголовке сверяется сервером.
Origin/Referer— дополнительный слой, но не замена токенов: заголовки могут отсутствовать или подделываться прокси.- CSRF не нужен при Bearer-токенах в
Authorization— браузер их не прикрепляет автоматически, но тогда токены живут в JS и уязвимы к XSS — фундаментальный компромисс.
Заключение
CSRF — атака не на сервер и не на фронтенд напрямую, а на механизм аутентификации браузера. Пока cookies привязаны к домену и отправляются автоматически, CSRF будет оставаться угрозой. Современная защита строится на трёх столпах: SameSite cookies блокируют большинство атак на уровне браузера, CSRF-токены защищают критичные операции, проверка Origin/Referer даёт дополнительный слой. Выбор между cookie-based auth + CSRF и Bearer + XSS-риск — фундаментальный компромисс, который нужно осознанно принимать при проектировании системы.