Перейти к содержимому

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.