CSRF (Cross-Site Request Forgery)
CSRF заставляет браузер жертвы выполнить нежелательный запрос к сайту, где она аутентифицирована, используя автоматически прикрепляемые cookies; понимание того, почему SameSite cookie закрывает большинство векторов, но не все — ключевой вопрос на security-ориентированных собеседованиях.
// ATTACK: CSRF через HTML-форму (Simple request — preflight не нужен!)// Страница злоумышленника на evil.com:
/*<html> <body onload="document.forms[0].submit()"> <form method="POST" action="https://bank.example.com/transfer" style="display:none"> <input type="hidden" name="to" value="attacker_account"> <input type="hidden" name="amount" value="50000"> </form> </body></html>*/
// Браузер автоматически прикрепляет session-cookie к запросу на bank.example.com.// POST-запрос — это "Simple request" (нет кастомных заголовков, application/x-www-form-urlencoded).// CORS preflight НЕ срабатывает → запрос доходит до сервера.// Сервер видит валидный session-cookie → выполняет перевод!// MITIGATION: CSRF-токен (Synchronizer Token Pattern)const crypto = require('crypto');
// Сервер: генерируем токен при рендеринге формыapp.get('/transfer', (req, res) => { const csrfToken = crypto.randomBytes(32).toString('hex'); req.session.csrfToken = csrfToken; res.render('transfer', { csrfToken }); // вставляем в hidden input});
// Сервер: проверяем токен перед обработкойapp.post('/transfer', (req, res) => { if (!req.body.csrfToken || req.body.csrfToken !== req.session.csrfToken) { return res.status(403).json({ error: 'CSRF token mismatch' }); } req.session.csrfToken = crypto.randomBytes(32).toString('hex'); // ротация после использования // выполнить перевод...});
// HTML шаблон: <input type="hidden" name="csrfToken" value="{{ csrfToken }}">// Злоумышленник не знает значение токена (SOP блокирует чтение ответа с bank.example.com)// MITIGATION: SameSite cookies + Double Submit Cookie для SPA// res.cookie('sid', sessionId, { sameSite: 'strict', httpOnly: true, secure: true });//// SameSite=Strict: cookie не отправляется ни при каком cross-site запросе// SameSite=Lax: отправляется при top-level GET-навигации (не при фоновых fetch/XHR)// SameSite=None: всегда (требует Secure)
// Double Submit Cookie: для SPA без server-side sessions// Сервер устанавливает CSRF-токен в НЕ-httpOnly cookie:// Set-Cookie: csrf_token=abc123; SameSite=Strict; Secure; Path=/
// Клиент читает cookie и добавляет в заголовокconst csrfToken = document.cookie.match(/csrf_token=([^;]+)/)?.[1];await fetch('/api/transfer', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken, // сервер сравнивает с cookie }, body: JSON.stringify({ to: 'alice', amount: 100 }),});// Злоумышленник не может добавить X-CSRF-Token из JS evil.com (SOP)// Форма evil.com не может прочитать cookie из bank.example.com (SOP)Итог: CSRF эксплуатирует автоматическую отправку cookies; основная защита — SameSite=Strict cookies + CSRF-токен для legacy форм, Double Submit Cookie для SPA.