CSRF (Cross-Site Request Forgery)
CSRF — атака, при которой злоумышленник заставляет браузер жертвы отправить запрос к доверенному сайту, используя авторизованную сессию жертвы.
Зачем нужно
CSRF позволяет выполнять действия от имени пользователя без его ведома: перевести деньги, сменить пароль, удалить аккаунт. Браузер автоматически прикладывает cookies к запросу — атакующий этим пользуется.
Где используется
Любое веб-приложение с аутентификацией на основе cookies: банкинг, e-commerce, CMS, соцсети, панели администрирования.
Предпосылки
HTTP-методы, cookies, HTML-формы, XSS
Как работает CSRF
1. Жертва авторизована на bank.com (cookie сессии в браузере)
2. Жертва заходит на evil.com (фишинговая ссылка, реклама)
3. evil.com содержит:
<form action="https://bank.com/transfer" method="POST">
<input type="hidden" name="to" value="attacker">
<input type="hidden" name="amount" value="10000">
</form>
<script>document.forms[0].submit();</script>
4. Браузер отправляет POST на bank.com С cookies жертвы
5. bank.com видит валидную сессию → выполняет перевод
Пример уязвимого сервера
// УЯЗВИМЫЙ КОД — нет защиты от CSRF
app.post('/transfer', (req, res) => {
const { to, amount } = req.body;
const user = req.session.user; // Cookie автоматически приложен
db.transfer(user.id, to, amount); // Выполняет перевод!
res.json({ success: true });
});
Варианты CSRF-атак
<!-- 1. Скрытая форма (POST) -->
<form action="https://bank.com/transfer" method="POST" id="csrfForm">
<input type="hidden" name="to" value="attacker">
<input type="hidden" name="amount" value="10000">
</form>
<script>document.getElementById('csrfForm').submit();</script>
<!-- 2. Через img (GET-запрос) -->
<img src="https://bank.com/transfer?to=attacker&amount=10000">
<!-- 3. Через iframe -->
<iframe src="https://bank.com/delete-account" style="display:none"></iframe>
Защита от CSRF
1. CSRF-токен (Synchronizer Token)
Сервер генерирует уникальный токен и вставляет в форму. При отправке проверяет.
// Сервер — генерация и проверка CSRF-токена
const crypto = require('crypto');
const csrf = require('csurf');
// Middleware
const csrfProtection = csrf({ cookie: true });
app.get('/transfer', csrfProtection, (req, res) => {
// Вставляем токен в форму
res.render('transfer', { csrfToken: req.csrfToken });
});
app.post('/transfer', csrfProtection, (req, res) => {
// csurf автоматически проверяет _csrf в body/query/header
const { to, amount } = req.body;
db.transfer(req.session.user.id, to, amount);
res.json({ success: true });
});
<!-- HTML-форма с CSRF-токеном -->
<form action="/transfer" method="POST">
<input type="hidden" name="_csrf" value="{{csrfToken}}">
<input name="to" placeholder="Кому">
<input name="amount" placeholder="Сумма">
<button type="submit">Перевести</button>
</form>
// Для SPA: токен в заголовке
fetch('/api/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken, // Получен из meta-тега или cookie
},
body: JSON.stringify({ to: 'bob', amount: 100 }),
});
2. SameSite Cookies
// Express — установка SameSite
res.cookie('session', token, {
httpOnly: true,
secure: true,
sameSite: 'Strict', // Cookie НЕ отправляется с кросс-сайтовых запросов
});
| SameSite | Поведение | Использование |
|---|---|---|
Strict |
Cookie не отправляется при переходе с другого сайта | Максимальная защита |
Lax |
Cookie отправляется только при навигации (GET) | Баланс UX и безопасности |
None |
Cookie отправляется всегда (нужен Secure) | Для кросс-сайтовых сценариев |
// Lax — хорошо для большинства случаев
res.cookie('session', token, {
httpOnly: true,
secure: true,
sameSite: 'Lax', // GET-навигация ОК, POST с другого сайта — нет
});
3. Double Submit Cookie
Токен хранится в cookie И отправляется в заголовке/body. Атакующий не может прочитать cookie другого домена.
// Сервер — установка CSRF-cookie
app.use((req, res, next) => {
if (!req.cookies.csrf) {
const token = crypto.randomBytes(32).toString('hex');
res.cookie('csrf', token, {
httpOnly: false, // JS должен прочитать!
secure: true,
sameSite: 'Lax',
});
}
next;
});
// Проверка
app.post('/api/*', (req, res, next) => {
const cookieToken = req.cookies.csrf;
const headerToken = req.headers['x-csrf-token'];
if (!cookieToken || cookieToken !== headerToken) {
return res.status(403).json({ error: 'CSRF token invalid' });
}
next;
});
// Клиент — читает cookie и кладёт в заголовок
function getCookie(name) {
const match = document.cookie.match(new RegExp(`${name}=([^;]+)`));
return match ? match[1] : null;
}
fetch('/api/transfer', {
method: 'POST',
headers: {
'X-CSRF-Token': getCookie('csrf'),
},
body: JSON.stringify(data),
});
4. Проверка заголовков Origin / Referer
app.use((req, res, next) => {
if (['POST', 'PUT', 'DELETE'].includes(req.method)) {
const origin = req.headers.origin || req.headers.referer;
const allowed = 'https://myapp.com';
if (!origin || !origin.startsWith(allowed)) {
return res.status(403).json({ error: 'Forbidden' });
}
}
next;
});
Чек-лист защиты
| Мера | Надёжность | Сложность |
|---|---|---|
| SameSite=Strict/Lax cookies | Высокая | Низкая |
| CSRF-токен | Высокая | Средняя |
| Double Submit Cookie | Высокая | Средняя |
| Проверка Origin/Referer | Средняя | Низкая |
| Не использовать GET для изменений | Базовая | Низкая |
Частые ошибки
1. Изменения через GET-запросы
// ПЛОХО: GET меняет данные
app.get('/delete-account', (req, res) => {
db.deleteUser(req.session.userId);
});
// Атака: <img src="https://app.com/delete-account">
// ХОРОШО: используй POST/DELETE
app.delete('/account', csrfProtection, (req, res) => {
db.deleteUser(req.session.userId);
});
2. SameSite=None без причины
// ПЛОХО: отключает защиту
res.cookie('session', token, { sameSite: 'None', secure: true });
// ХОРОШО: Lax или Strict
res.cookie('session', token, { sameSite: 'Lax', secure: true });
3. Предсказуемые CSRF-токены
// ПЛОХО: токен на основе timestamp
const token = Date.now().toString();
// ХОРОШО: криптографически случайный
const token = crypto.randomBytes(32).toString('hex');
Практика
- Создай Express-приложение с формой перевода и реализуй CSRF-токен через csurf
- Настрой SameSite=Lax для session cookie
- Реализуй Double Submit Cookie паттерн вручную (без библиотек)
- Создай тестовую HTML-страницу, имитирующую CSRF-атаку, и убедись, что защита работает