Перейти к основному содержанию
Блог

Веб

CSRF и SameSite cookie: запросы от имени пользователя

Как работает CSRF, насколько от него защищает SameSite, CSRF-токены, API с cookie и bearer-токенами и связь с CORS. С примерами для Laravel и Node.

· 9 мин. чтения

Фото: Gugalcrom123, CC BY-SA 4.0 · Wikimedia Commons

Cross-Site Request Forgery (CSRF) — атака, при которой пользователь, сам того не зная, отправляет на ваш сайт запрос со своей сессией. Пользователь вошёл в вашу систему, а затем открыл другую страницу. Эта страница через скрытую форму отправляет POST на ваш сайт. Браузер по привычке добавляет к запросу cookie вашего сайта — и сервер принимает это за действие настоящего пользователя.

За последние годы браузеры ввели атрибут cookie SameSite, и CSRF стал заметно сложнее. Но вывод «CSRF больше нет» неверен. В этой статье разберём, как работает CSRF, что закрывает SameSite и что не закрывает, а также связь между токенами, API и CORS.

Как выглядит атака

Представьте, что в приложении есть эндпоинт смены email:

POST /profile/email
Cookie: session=...
email=new@example.uz

Атакующий размещает на своей странице:

<form action="https://app.example.uz/profile/email" method="POST" id="f">
  <input type="hidden" name="email" value="attacker@evil.example">
</form>
<script>document.getElementById('f').submit();</script>

Если пользователь вошёл в систему, а сервер полагается только на сессионную cookie, email изменится. Затем атакующий захватывает аккаунт через «восстановление пароля». Тем же способом можно переводить деньги, менять роли, удалять данные.

Обратите внимание: атакующий не может прочитать ответ. CSRF — «слепая» атака, её цель не кража данных, а изменение состояния.

Почему это важно для экспертизы

CSRF классифицируется как CWE-352 и в OWASP Top 10 (2021) отнесён к категории A01 «Broken Access Control». OWASP ASVS отдельно требует защиты от CSRF для запросов, меняющих состояние. На экспертизе обычно проверяют:

  • Есть ли CSRF-токен в формах, меняющих состояние, и действительно ли сервер его проверяет.
  • Установлены ли атрибуты SameSite, Secure, HttpOnly у сессионных cookie.
  • Не меняется ли состояние через GET-запрос.
  • Не позволяют ли настройки CORS чужому домену отправить запрос с cookie и прочитать ответ.

SameSite: что закрывает, а что нет

Атрибут SameSite говорит браузеру, добавлять ли cookie к запросам, инициированным с другого сайта:

  • Strict — cookie отправляется только в запросах внутри вашего сайта. Даже при переходе по ссылке с другого сайта пользователь окажется «не вошедшим».
  • Lax — при обычной навигации с другого сайта (GET по ссылке) cookie отправляется, но в POST-формах, fetch, iframe и картинках — нет.
  • None — cookie отправляется везде. В этом случае Secure обязателен.

Большинство современных браузеров считают cookie без атрибута как Lax. Laravel тоже по умолчанию использует lax для сессионной cookie (same_site в config/session.php).

Lax закрывает классический POST-CSRF. Но это не полная защита:

  1. Изменение состояния через GET. Если /logout, /delete?id=5 или /approve?id=7 работают через GET, Lax отправит cookie, и атака сработает.
  2. Разница между «site» и «origin». SameSite работает на уровне сайта: app.example.uz и blog.example.uz считаются одним сайтом. Если на одном из поддоменов есть XSS или он передан третьей стороне, запрос оттуда будет «same-site», и SameSite не поможет.
  3. Случаи, когда нужен SameSite=None. Если приложение встраивается в iframe на другом сайте или есть сторонняя интеграция, cookie переводят в None, и защита пропадает.
  4. Старые браузеры и нестандартные клиенты не всегда трактуют атрибут одинаково.

SameSite — хороший дополнительный уровень, но он не заменяет CSRF-токен.

CSRF-токен: основная защита

Классическая защита — synchronizer token: сервер записывает в сессию случайный токен, добавляет его в каждую форму скрытым полем и проверяет при каждом запросе, меняющем состояние. Чужой сайт не знает этот токен и не может его прочитать.

Laravel

В Laravel CSRF-защита включена по умолчанию в группе middleware web. В формах Blade:

<form method="POST" action="/profile/email">
    @csrf
    <input type="email" name="email">
</form>

Axios и Inertia читают cookie XSRF-TOKEN и автоматически отправляют её в заголовке X-XSRF-TOKEN. Главное, что нужно проверить, — список исключений. В Laravel 11+ в bootstrap/app.php:

->withMiddleware(function (Middleware $middleware) {
    $middleware->validateCsrfTokens(except: [
        'webhooks/payment',   // только настоящие внешние вебхуки
    ]);
})

Добавляйте в исключения только вебхуки, которые вызывают внешние сервисы, и проверяйте их подписью (HMAC). 'api/*' или '*' по принципу «не работало — добавил в исключения» — распространённая и серьёзная ошибка.

Node (Express)

Широко использовавшийся раньше пакет csurf помечен как устаревший. Вместо него используйте библиотеки, реализующие шаблон double-submit cookie, или напишите минимальную защиту сами: проверку заголовка Origin и токен.

const ALLOWED_ORIGINS = new Set(['https://app.example.uz']);

function verifyOrigin(req, res, next) {
  if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) return next();
  const origin = req.get('Origin') || '';
  if (!ALLOWED_ORIGINS.has(origin)) {
    return res.status(403).json({ error: 'Invalid origin' });
  }
  next();
}

app.use(verifyOrigin);

Проверка Origin хорошо работает в современных браузерах: в POST-запросах браузер ставит этот заголовок сам, и JavaScript не может его подделать. Дополнительно можно использовать заголовок Sec-Fetch-Site: отклоняйте запросы, меняющие состояние, со значением cross-site.

Для сессионной cookie:

app.use(session({
  secret: process.env.SESSION_SECRET,
  cookie: { httpOnly: true, secure: true, sameSite: 'lax' },
}));

Корень CSRF — браузер добавляет cookie автоматически. Поэтому решающее значение имеет то, как API аутентифицирует запросы:

  • Bearer-токен (Authorization: Bearer ...) — браузер сам этот заголовок не добавляет, его должен поставить JavaScript. Чужой сайт токена не знает, значит CSRF практически невозможен. Но если токен хранится в localStorage, его может украсть XSS — это другой риск.
  • API на cookie (SPA, Sanctum stateful) — браузер отправляет cookie автоматически, значит защита от CSRF нужна. Поэтому в режиме SPA в Laravel Sanctum сначала вызывается /sanctum/csrf-cookie, а следующие запросы отправляются с X-XSRF-TOKEN.

В Sanctum указывайте в SANCTUM_STATEFUL_DOMAINS только свои фронтенд-домены. Для мобильного приложения используйте режим токенов, а не cookie.

Частая ошибка: проверку CSRF отключают по логике «API принимает JSON, значит формой атаковать нельзя». Если сервер не проверяет строго Content-Type и читает тело text/plain или application/x-www-form-urlencoded как JSON, атака обычной формой снова становится возможной.

Связь с CORS

CORS и CSRF часто путают. Важные моменты:

  1. CORS не защищает от CSRF. CORS управляет только тем, может ли чужая страница прочитать ответ. Простые запросы (POST формы, text/plain) отправляются без preflight, и сервер их выполняет — даже если ответ не будет прочитан.
  2. Неправильный CORS усиливает CSRF. Если сервер отражает любой Origin и возвращает Access-Control-Allow-Credentials: true, чужой сайт может отправить запрос с cookie и прочитать ответ — включая CSRF-токен. Тогда падает и защита токеном.
  3. Preflight полезен, но не является единственной защитой. Content-Type: application/json или собственный заголовок вызывают preflight, и правильно настроенный CORS его отклонит. Но это работает, только если сервер строго проверяет Content-Type.

В Laravel config/cors.php:

'paths' => ['api/*', 'sanctum/csrf-cookie'],
'allowed_origins' => ['https://app.example.uz'],
'supports_credentials' => true,

Нельзя ставить * в allowed_origins и одновременно supports_credentials в true — настройте это правильно.

Как проверить самостоятельно

Действительно ли проверяется токен

Войдите в систему в браузере, через DevTools скопируйте запрос, меняющий состояние («Copy as cURL»), затем отправьте его повторно без токена:

# С токеном — ожидается 200/302
curl -si -X POST https://staging.example.uz/profile/email \
  -H "Cookie: laravel_session=..." \
  -d "_token=VALID&email=test@example.uz"

# Без токена — ожидается 419 (Laravel) или 403
curl -si -X POST https://staging.example.uz/profile/email \
  -H "Cookie: laravel_session=..." \
  -d "email=test@example.uz"

Если второй запрос прошёл успешно, защита не работает.

Origin и CORS

curl -si https://staging.example.uz/api/user \
  -H "Origin: https://evil.example" \
  -H "Cookie: laravel_session=..." | grep -i "access-control"

Если в ответе вместе появляются Access-Control-Allow-Origin: https://evil.example и Access-Control-Allow-Credentials: true, это серьёзная проблема.

curl -si https://staging.example.uz/login | grep -i set-cookie

У сессионной cookie должны быть Secure, HttpOnly и SameSite=Lax (или Strict).

Изменение состояния через GET

Просмотрите список маршрутов:

php artisan route:list --method=GET | grep -iE "delete|remove|logout|approve|toggle"

Действия, меняющие состояние, должны выполняться через POST, PUT, PATCH или DELETE.

Короткий чек-лист

  • Все запросы, меняющие состояние, защищены CSRF-токеном или проверкой Origin.
  • В исключениях CSRF только вебхуки, проверяемые подписью.
  • GET-запросы не меняют состояние.
  • У сессионных cookie установлены Secure, HttpOnly, SameSite=Lax или Strict.
  • Для SPA/API на cookie включена защита от CSRF, stateful-домены Sanctum указаны явно.
  • Сервер строго проверяет Content-Type.
  • CORS не отражает Origin, credentials разрешены только для конкретных доменов.
  • Поддомены под контролем, на них проверено отсутствие XSS.

Вывод

SameSite cookie заметно усложнили CSRF, но не устранили его. Надёжная защита состоит из нескольких уровней: CSRF-токен или проверка Origin, изменение состояния только через POST/PUT/DELETE, правильные атрибуты cookie и строгий CORS. Эти настройки часто уже есть, но перестают работать из-за списка исключений или «временно» отключённой проверки — именно такие места и находит экспертиза.

Хотите проверить CSRF-защиту форм и API до экспертизы? Оставьте заявку на сканирование — Claude AI агент просмотрит маршруты, middleware и настройки cookie, а наш специалист поможет закрыть найденные дыры.