Що таке CSRF і як його запобігти?
CSRF (Cross-Site Request Forgery) - це вразливість веб-безпеки, яка дозволяє зловмисникам обманом змусити користувачів виконувати небажані дії на сайті, де вони автентифіковані.
Як працює CSRF
Сценарій атаки
- Користувач логінитьсяна
bank.com(отримує session cookie) - Користувач відвідує зловмисний сайт
evil.com(залишаючись залогіненим на bank.com) evil.comвідправляє підроблений запит наbank.com- Браузер автоматично включає cookies з bank.com
- Банк обробляє запит, ніби користувач ініціював його
Приклад атаки
Вразливий переказ коштів:
<!-- На evil.com -->
<img src="https://bank.com/transfer?to=attacker&amount=1000" />
<!-- або -->
<form action="https://bank.com/transfer" method="POST">
<input type="hidden" name="to" value="attacker" />
<input type="hidden" name="amount" value="1000" />
</form>
<script>
document.forms[0].submit();
</script>Коли користувач завантажує цю сторінку, запит автоматично відправляється з їхніми cookies з bank.com!
Методи запобігання
1. CSRF токени (найпоширеніше)
Генерування унікального токена для кожної сесії/запиту:
Backend (Node.js/Express):
const csrf = require('csurf');
const csrfProtection = csrf({ cookie: true });
app.get('/form', csrfProtection, (req, res) => {
// Генеруємо CSRF токен
res.render('form', { csrfToken: req.csrfToken() });
});
app.post('/transfer', csrfProtection, (req, res) => {
// Токен автоматично валідується
if (valid) {
processTransfer(req.body);
}
});Frontend:
<form action="/transfer" method="POST">
<input type="hidden" name="_csrf" value="{{ csrfToken }}" />
<input name="to" />
<input name="amount" />
<button>Перевести</button>
</form>З React/Fetch:
function Transfer() {
const [csrfToken, setCsrfToken] = useState('');
useEffect(() => {
fetch('/api/csrf-token')
.then(r => r.json())
.then(data => setCsrfToken(data.token));
}, []);
const handleTransfer = async () => {
await fetch('/api/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'CSRF-Token': csrfToken // Кастомний заголовок
},
body: JSON.stringify({ to, amount })
});
};
return <button onClick={handleTransfer}>Перевести</button>;
}2. SameSite Cookies
Запобігання відправці cookies браузером з cross-site запитами:
// Node.js/Express
res.cookie('sessionId', sessionId, {
httpOnly: true,
secure: true,
sameSite: 'strict' // або 'lax'
});Значення SameSite:
strict: Cookie ніколи не відправляється на cross-site запитиlax: Cookie відправляється на top-level навігацію (лише GET)none: Cookie відправляється на всі запити (потребуєsecure)
3. Кастомні заголовки запитів
Вимагати кастомні заголовки, які не можуть бути встановлені формами:
// Frontend
fetch('/api/transfer', {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
// Backend
app.post('/api/transfer', (req, res) => {
if (req.headers['x-requested-with'] !== 'XMLHttpRequest') {
return res.status(403).send('Заборонено');
}
// Обробка переказу
});4. Перевірка Origin/Referer заголовків
app.post('/api/transfer', (req, res) => {
const origin = req.headers.origin || req.headers.referer;
if (!origin || !origin.startsWith('https://bank.com')) {
return res.status(403).send('Невірний origin');
}
// Обробка переказу
});5. Повторна автентифікація для чутливих дій
app.post('/api/delete-account', async (req, res) => {
// Вимагаємо підтвердження паролем
const user = await User.findById(req.userId);
if (!await user.checkPassword(req.body.password)) {
return res.status(401).send('Невірний пароль');
}
// Видаляємо акаунт
});Реальний приклад: повний захист
// Express.js з множинними захистами
const express = require('express');
const csrf = require('csurf');
const cookieParser = require('cookie-parser');
const app = express();
// 1. SameSite cookies
app.use(cookieParser());
app.use((req, res, next) => {
res.cookie('session', req.sessionID, {
httpOnly: true,
secure: true,
sameSite: 'strict'
});
next();
});
// 2. CSRF токени
const csrfProtection = csrf({ cookie: true });
// 3. Перевірка origin
const verifyOrigin = (req, res, next) => {
const origin = req.headers.origin;
const allowedOrigins = ['https://myapp.com'];
if (origin && !allowedOrigins.includes(origin)) {
return res.status(403).send('Заборонено');
}
next();
};
// Захищений роут з множинними захистами
app.post('/api/transfer',
verifyOrigin,
csrfProtection,
(req, res) => {
// Обробка переказу
}
);Тестування на вразливості CSRF
<!-- Тестова сторінка для перевірки вразливості сайту -->
<!DOCTYPE html>
<html>
<body>
<h1>CSRF тест</h1>
<form id="testForm" action="https://target-site.com/api/action" method="POST">
<input type="hidden" name="data" value="malicious" />
</form>
<script>
// Авто-відправка при завантаженні сторінки
document.getElementById('testForm').submit();
</script>
</body>
</html>Якщо дія виконується без додаткової перевірки, сайт вразливий!
Типові питання на співбесіді
-
П: Яка різниця між CSRF і XSS? В: CSRF експлуатує автентифікацію користувача для виконання дій. XSS впроваджує зловмисні скрипти. CSRF використовує користувача як "заплутаного заступника", XSS атакує користувача напряму.
-
П: Чому зловмисники не можуть прочитати CSRF токен зі сторінки? В: Через Same-Origin Policy, JavaScript з evil.com не може читати вміст з bank.com.
-
П: Чи достатньо HTTPS для запобігання CSRF? В: Ні! HTTPS запобігає MITM атакам, але не зупиняє CSRF. Вам все одно потрібні CSRF токени або інші захисти.
-
П: Чи можуть GET запити мати вразливості CSRF? В: Так! Якщо GET запити змінюють стан (видалення, переказ тощо), вони вразливі. Завжди використовуйте POST/PUT/DELETE для зміни стану.
Найкращі практики
✅ РОБІТЬ:
- Використовуйте CSRF токени для операцій зміни стану
- Встановлюйте SameSite=Strict для session cookies
- Перевіряйте Origin/Referer заголовки
- Використовуйте POST/PUT/DELETE для зміни стану (ніколи GET)
- Вимагайте повторну автентифікацію для чутливих дій
❌ НЕ РОБІТЬ:
- Покладайтеся лише на HTTPS
- Використовуйте GET для операцій зміни стану
- Довіряйте лише cookies для автентифікації
- Забувайте валідувати токени на кожному запиті
- Зберігайте CSRF токени в cookies (використовуйте приховані поля або кастомні заголовки)
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.