13. CSRF: атаки и защита
Что такое CSRF?
Заголовок раздела «Что такое CSRF?»CSRF (Cross-Site Request Forgery) — атака, при которой злоумышленник заставляет браузер авторизованного пользователя выполнить нежелательные действия на другом сайте.
Сценарий атаки:
- Пользователь залогинен в bank.com (куки активны)
- Пользователь открывает evil.com
- evil.com содержит скрытую форму или изображение, отправляющее запрос на bank.com
- Браузер автоматически включает куки bank.com в запрос
- bank.com думает, что это легитимный запрос от пользователя
<!-- evil.com — невидимая форма автоматически отправляется --><form action="https://bank.com/transfer" method="POST" id="evil"> <input name="to" value="attacker-account"> <input name="amount" value="10000"></form><script>document.getElementById('evil').submit();</script>Важно: CSRF работает именно потому, что браузер автоматически отправляет куки — включая куки сессии!
Методы защиты
Заголовок раздела «Методы защиты»1. SameSite Cookie (главная защита)
Заголовок раздела «1. SameSite Cookie (главная защита)»SameSite атрибут куки контролирует, когда браузер отправляет куку.
res.cookie('sessionId', token, { sameSite: 'Lax', // защита от CSRF! httpOnly: true, secure: true,});| SameSite | Описание | CSRF защита |
|---|---|---|
Strict | Кука не отправляется никогда с других сайтов | Максимальная |
Lax | Отправляется только при навигации GET (ссылки) | Хорошая (default) |
None | Всегда отправляется (нужен Secure) | Нет защиты |
С 2020 года Lax — значение по умолчанию в Chrome. Это уже частичная защита.
2. CSRF Токены
Заголовок раздела «2. CSRF Токены»Классический метод: уникальный токен в каждой форме.
import crypto from 'crypto';
// Генерация CSRF токенаfunction generateCSRFToken() { return crypto.randomBytes(32).toString('hex');}
// Middleware: добавляем токен в сессию и cookieapp.use((req, res, next) => { if (!req.session.csrfToken) { req.session.csrfToken = generateCSRFToken(); }
// Доступный JavaScript cookie (не httpOnly!) res.cookie('csrf-token', req.session.csrfToken, { httpOnly: false, // JavaScript должен читать его secure: true, sameSite: 'Strict', });
next();});
// Middleware: проверка CSRF токена для изменяющих запросовfunction csrfProtection(req, res, next) { if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) { return next(); // читающие запросы не проверяем }
const token = req.headers['x-csrf-token'] || req.body._csrf;
if (!token || token !== req.session.csrfToken) { return res.status(403).json({ error: 'Invalid CSRF token' }); }
next();}
app.use(csrfProtection);<!-- В форме --><form method="POST" action="/transfer"> <input type="hidden" name="_csrf" value="<%= csrfToken %>"> <input name="amount"> <button type="submit">Перевести</button></form>3. Double Submit Cookie Pattern
Заголовок раздела «3. Double Submit Cookie Pattern»Токен в cookie + токен в запросе:
// Клиент читает CSRF из cookie и добавляет в заголовокfunction getCookie(name) { return document.cookie.split(';') .find(c => c.trim().startsWith(name + '=')) ?.split('=')[1];}
// Axios interceptoraxios.interceptors.request.use(config => { if (!['get', 'head', 'options'].includes(config.method)) { config.headers['X-CSRF-Token'] = getCookie('csrf-token'); } return config;});
// fetch wrapperasync function secureFetch(url, options = {}) { const csrfToken = getCookie('csrf-token');
return fetch(url, { ...options, headers: { ...options.headers, 'X-CSRF-Token': csrfToken, }, credentials: 'include', });}4. CSRF защита в Express (csurf)
Заголовок раздела «4. CSRF защита в Express (csurf)»# csurf устарел, используй csrf пакетnpm install csrfimport csrf from 'csrf';import cookieParser from 'cookie-parser';
const tokens = new csrf();
app.use(cookieParser());
// Генерируем секрет при первом визитеapp.use((req, res, next) => { let secret = req.cookies['csrf-secret'];
if (!secret) { secret = tokens.secretSync(); res.cookie('csrf-secret', secret, { httpOnly: true, secure: true, sameSite: 'Strict', }); }
// Создаём токен из секрета res.locals.csrfToken = tokens.create(secret); next();});
// Проверяем токенapp.use((req, res, next) => { if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) return next();
const secret = req.cookies['csrf-secret']; const token = req.headers['x-csrf-token'] || req.body._csrf;
if (!tokens.verify(secret, token)) { return res.status(403).json({ error: 'Invalid CSRF token' }); }
next();});5. SameSite + Origin/Referer проверка
Заголовок раздела «5. SameSite + Origin/Referer проверка»function checkOrigin(req, res, next) { if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) return next();
const origin = req.headers.origin; const referer = req.headers.referer;
const allowedOrigins = [ 'https://myapp.com', 'https://www.myapp.com', ];
const requestOrigin = origin || (referer && new URL(referer).origin);
if (!requestOrigin || !allowedOrigins.includes(requestOrigin)) { return res.status(403).json({ error: 'Cross-site request blocked' }); }
next();}CSRF в Next.js
Заголовок раздела «CSRF в Next.js»Для API Routes
Заголовок раздела «Для API Routes»// middleware.ts или в каждом API роутеimport { NextRequest, NextResponse } from 'next/server';
function isCsrfSafe(req: NextRequest): boolean { // GET, HEAD, OPTIONS — безопасные if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) return true;
// Проверяем Origin const origin = req.headers.get('origin'); const host = req.headers.get('host');
if (origin) { const originHost = new URL(origin).host; return originHost === host; }
// Проверяем Referer как fallback const referer = req.headers.get('referer'); if (referer) { const refererHost = new URL(referer).host; return refererHost === host; }
return false;}
export function middleware(req: NextRequest) { if (req.nextUrl.pathname.startsWith('/api/')) { if (!isCsrfSafe(req)) { return NextResponse.json( { error: 'Cross-site request blocked' }, { status: 403 } ); } }}Использование JWT вместо куки
Заголовок раздела «Использование JWT вместо куки»JWT в Authorization заголовке — естественная защита от CSRF:
// CSRF атака не работает: злоумышленник не может прочитать localStorage// и не может добавить Authorization заголовок в cross-site запросе
fetch('/api/transfer', { method: 'POST', headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ amount: 100 }),});Но! Хранение JWT в localStorage уязвимо к XSS. Гибрид: refresh token в httpOnly SameSite cookie + access token в памяти.
Проверка уязвимости
Заголовок раздела «Проверка уязвимости»<!-- Тест: открой это в другой вкладке пока залогинен --><!DOCTYPE html><html><body> <form action="https://yourapp.com/api/change-email" method="POST"> </form> <script>document.forms[0].submit();</script></body></html>Если запрос прошёл — у тебя CSRF уязвимость!
Итог: Чеклист защиты от CSRF
Заголовок раздела «Итог: Чеклист защиты от CSRF»- Куки с
SameSite=Lax(минимум) илиStrict - CSRF токен в формах (если нет SameSite)
- Origin/Referer проверка для API
- JWT в заголовке вместо куки (для API)
- Важные действия требуют повторного подтверждения пароля
Практические задания
Заголовок раздела «Практические задания»- Настрой
SameSite=Strictдля сессионных куки - Реализуй CSRF токен для HTML форм в Express
- Добавь Origin проверку для API в Next.js middleware
- Протестируй свой сайт на CSRF уязвимость