Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Що таке CSRF і як його запобігти?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**CSRF (Cross-Site Request Forgery)** - це атака, при якій зловмисний сайт змушує браузер відправляти автентифіковані запити на інший сайт. Браузер відправляє cookies cross-site автоматично, тому сервер не розрізняє підроблені та справжні запити. Захист: CSRF токени для запитів зміни стану і `SameSite=Strict` на session cookie.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**CSRF (Cross-Site Request Forgery)** - це атака, при якій зловмисний сайт змушує браузер користувача відправляти автентифіковані запити на інший сайт без його відома. ## Теорія ### Коротко - Браузер автоматично відправляє cookies на будь-який сайт, що їх встановив, незалежно від того, звідки надійшов запит - Аналогія: дворецький, який віддає ключі від будинку будь-кому, хто запитає, бо адресу знають усі - Сервер бачить валідний session cookie і не може відрізнити запит з твоєї форми від підробленого з `evil.com` - Рішення: прикріплювати до кожного запиту зміни стану унікальний секретний токен, який зловмисник не може прочитати або вгадати - `SameSite=Strict` на session cookie блокує більшість сучасних CSRF атак без зайвого коду ### Швидкий приклад ```javascript // Вразливо: перевірки немає - зловмисник на evil.com підробить цей POST app.post('/transfer', (req, res) => { const { to, amount } = req.body; // req.session.user встановлено через cookie - браузер відправляє його автоматично transferMoney(req.session.user.id, to, amount); // спрацює і для підробленого запиту res.send('Transfer complete'); }); // Захищено: csurf блокує запити без валідного токена const csurf = require('csurf'); app.use(csurf({ cookie: true })); app.post('/transfer', (req, res) => { // Відсутній або невірний _csrf токен -> 403 Forbidden, сюди запит не дійде transferMoney(req.session.user.id, req.body.to, req.body.amount); res.send('Transfer complete'); }); ``` Різниця в одному рядку middleware. Без нього будь-яка сторінка в мережі може відправити POST на твій endpoint і використати session cookie користувача. ### Як працює атака Браузери дотримуються Same-Origin Policy для читання даних, але cookies з неї виключені. Коли браузер відкриває `evil.com` і там є `<img src="https://bank.com/transfer?to=attacker&amount=1000">`, браузер надсилає GET запит і автоматично прикріплює session cookie з `bank.com`. Сервер банку бачить валідну сесію та обробляє запит. POST атаки трохи складніше організувати, але вони так само ефективні: ```html <!-- На evil.com --> <form id="f" action="https://bank.com/transfer" method="POST"> <input type="hidden" name="to" value="attacker" /> <input type="hidden" name="amount" value="1000" /> </form> <script>document.getElementById('f').submit();</script> <!-- Користувач нічого не бачить. Переказ проходить. --> ``` Користувач відкриває сторінку, форма відправляється у фоні, гроші переказані. Сервер не отримує жодного сигналу про те, що форма була на `evil.com`, а не на `bank.com`. ### Коли застосовувати захист - Операції зміни стану (POST, PUT, DELETE) на автентифікованих endpoints: токен обов'язковий - JSON API: `Content-Type: application/json` сам по собі не захищає (дивись типові помилки нижче) - Read-only GET endpoints: захист не потрібен, але переконайся що вони справді тільки читають - Single-page застосунки: поєднуй `SameSite=Strict` cookies з кастомним заголовком `X-CSRF-Token` - Мікросервіси з JWT: зберігай токен у заголовку `Authorization`, а не в cookie, і CSRF поверхня зникає ### Методи захисту **CSRF токени** - найпоширеніший підхід. Сервер генерує випадкове значення, прив'язане до сесії, і вбудовує його в кожну форму. При відправці перевіряє збіг. Зловмисник на `evil.com` не може прочитати це значення через Same-Origin Policy. ```javascript // Express + csurf (csurf v1.11.0) const csurf = require('csurf'); app.use(csurf({ cookie: true })); // Передаємо токен у React SPA app.get('/csrf', (req, res) => res.json({ token: req.csrfToken() })); // React форма, яка отримує токен при монтуванні function ProfileForm() { const [csrfToken, setCsrfToken] = useState(''); useEffect(() => { fetch('/csrf').then(r => r.json()).then(d => setCsrfToken(d.token)); }, []); return ( <form action="/profile" method="POST"> <input type="hidden" name="_csrf" value={csrfToken} /> <input name="email" /> <button>Оновити</button> </form> ); // Підроблений POST з evil.com -> 403, токена немає } ``` **SameSite cookies** - сучасне доповнення. Встанови `sameSite: 'strict'` на session cookie і браузер просто не відправлятиме його на cross-site запити. ```javascript res.cookie('sessionId', id, { httpOnly: true, secure: true, sameSite: 'strict' }); ``` `Strict` блокує відправку cookie на всіх cross-site запитах. `Lax` дозволяє cookies при top-level GET навігації (перехід за посиланням), але блокує cross-site POST. `None` відправляє cookies скрізь і вимагає `Secure`. Часто бачу підхід: поставили SameSite і вважають що цього досить. На сучасному Chrome в більшості випадків і справді вистачить. Але Apple повністю виправила реалізацію SameSite тільки в Safari 16.4, а додати токен - це десять хвилин роботи. Краще мати обидва рівні. **Подвійний cookie (double-submit cookie)** підходить коли немає серверного session store. Генеруєш випадковий токен, кладеш у cookie і також вимагаєш у заголовку запиту або полі форми. Сервер перевіряє що обидва значення збігаються. Зловмисник не може прочитати cookie з іншого origin, тому не може надати обидва одночасно. ### Типові помилки **Захищати форми, але ігнорувати AJAX запити.** `fetch()` з `Content-Type: application/json` автоматично не захищає. Браузери дозволяють cross-site JSON POST в деяких конфігураціях. ```javascript // Зловмисник на evil.com fetch('https://bank.com/api/transfer', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ to: 'attacker', amount: 1000 }) }); // Якщо сервер приймає JSON без перевірки X-CSRF-Token - це спрацює ``` Рішення: вимагай `X-CSRF-Token` у заголовках для всіх API routes, не тільки для HTML форм. **Використовувати статичні токени.** Токен корисний тільки якщо він непередбачуваний і прив'язаний до сесії. Захардкоджений рядок типу `'static-token-123'` можна вкрасти через XSS або підібрати. Використовуй `req.csrfToken()`, який генерує нове значення при кожному виклику. **GET запити для зміни стану.** Будь-який тег `<img>` може відправити GET запит cross-site без будь-якої взаємодії з користувачем. ```javascript // Неправильно: зміна стану через GET app.get('/user/delete', (req, res) => deleteUser(req.session.userId)); // Правильно: POST з CSRF токеном app.post('/user/delete', csrfProtection, (req, res) => deleteUser(req.session.userId)); ``` **Покладатися тільки на заголовки Origin/Referer.** Проксі та інструменти приватності видаляють ці заголовки. Деякі браузери не відправляють їх взагалі. OWASP позначає це як ненадійний самостійний захист. Використовуй як додатковий рівень поверх токенів. **Встановити `SameSite=Lax` скрізь і вважати себе захищеним.** `Lax` все ще дозволяє cookies при top-level POST навігації, тобто шкідливе посилання може спрацювати в деяких випадках. ### Де застосовується - Express.js: middleware `csurf`, використовується в KeystoneJS і понад 1M npm проектів - Django: декоратор `@csrf_protect`, увімкнений глобально за замовчуванням (Instagram admin) - Rails: `protect_from_forgery`, вбудований (GitHub, Shopify) - Spring Boot: `@EnableWebSecurity` з `CsrfTokenRepository` - Next.js: `next-csrf` для API routes у Vercel шаблонах Для мікросервісів з JWT: зберігай токен у заголовку `Authorization`. Браузер не прикріплює кастомні заголовки до cross-site запитів автоматично, тому CSRF вектор відсутній. ### Питання на співбесіді **Q:** Яка різниця між CSRF і XSS? **A:** XSS впроваджує шкідливий код прямо на твій сайт і може вкрасти токени безпосередньо. CSRF використовує наявну автентифікацію з іншого origin, нічого не впроваджуючи. XSS порушує Same-Origin Policy. CSRF експлуатує виключення для cookies в ній. **Q:** Як `SameSite=Strict` запобігає CSRF? **A:** Браузер не відправлятиме session cookie на жодному cross-site запиті, включаючи підроблені POST форми. Без cookie сервер бачить неавтентифікований запит і відхиляє його. Chrome встановив `SameSite=Lax` за замовчуванням починаючи з версії 80. **Q:** Що таке техніка double-submit cookie? **A:** Генеруєш випадковий токен, кладеш у cookie і також вимагаєш у заголовку запиту або полі форми. Сервер перевіряє що обидва значення збігаються. Зловмисник не може прочитати cookie з іншого origin, тому не може надати обидва значення одночасно. **Q:** Чому не можна покладатися тільки на заголовки Origin і Referer? **A:** Проксі та інструменти приватності видаляють ці заголовки. Деякі браузери не відправляють їх взагалі. OWASP позначає це як ненадійний самостійний захист. Використовуй як додатковий рівень поверх токенів. **Q:** Як організувати захист від CSRF в мікросервісах з JWT на рівні API gateway? **A:** Зберігай JWT у заголовку `Authorization`, а не в cookie. Браузер не додає кастомні заголовки до cross-site запитів автоматично, тому CSRF ризику немає. Якщо JWT треба зберігати в cookie, додай підписаний заголовок `X-CSRF-Token` і валідуй його на кожному сервісі. ## Приклади ### Базовий: Як виглядає атака ```html <!-- evil.com - тиха авто-відправка при завантаженні сторінки --> <form id="f" action="https://bank.com/transfer" method="POST"> <input type="hidden" name="to" value="attacker@evil.com" /> <input type="hidden" name="amount" value="5000" /> </form> <script>document.getElementById('f').submit();</script> <!-- Користувач бачить порожню сторінку. Переказ вже пройшов. --> ``` Якщо `bank.com` не перевіряє CSRF токен, переказ виконується. Браузер зробив саме те, що йому сказали, з повною автентифікацією в запиті. ### Середній: Захищена форма переказу в React ```jsx // Клієнт: отримує CSRF токен перед рендером форми function TransferForm() { const [token, setToken] = useState(''); const [to, setTo] = useState(''); const [amount, setAmount] = useState(''); useEffect(() => { fetch('/csrf').then(r => r.json()).then(d => setToken(d.token)); }, []); const handleSubmit = async (e) => { e.preventDefault(); await fetch('/api/transfer', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': token // зловмисник на evil.com не може отримати це значення }, body: JSON.stringify({ to, amount }) }); }; return ( <form onSubmit={handleSubmit}> <input value={to} onChange={e => setTo(e.target.value)} placeholder="Отримувач" /> <input value={amount} onChange={e => setAmount(e.target.value)} placeholder="Сума" /> <button type="submit">Перевести</button> </form> ); } ``` ```javascript // Сервер: видає токен і перевіряє при кожному POST app.get('/csrf', csrfProtection, (req, res) => res.json({ token: req.csrfToken() })); app.post('/api/transfer', csrfProtection, (req, res) => { transferMoney(req.session.user.id, req.body.to, req.body.amount); res.json({ status: 'ok' }); // 403 якщо X-CSRF-Token відсутній або не збігається з токеном сесії }); ``` ### Розширений: Три рівні захисту в Express ```javascript const express = require('express'); const csurf = require('csurf'); const cookieParser = require('cookie-parser'); const app = express(); app.use(express.json()); app.use(cookieParser()); // Рівень 1: SameSite session cookie - блокує більшість browser-based атак app.use((req, res, next) => { res.cookie('sessionId', req.sessionID, { httpOnly: true, secure: true, sameSite: 'strict' }); next(); }); // Рівень 2: CSRF токен - перехоплює те, що пропустив SameSite const csrfProtection = csurf({ cookie: true }); // Рівень 3: перевірка Origin як додатковий бар'єр const checkOrigin = (req, res, next) => { const origin = req.headers.origin; if (origin && !['https://myapp.com'].includes(origin)) { return res.status(403).json({ error: 'Origin not allowed' }); } next(); }; // Три рівні послідовно на чутливому route app.post('/api/transfer', checkOrigin, csrfProtection, (req, res) => { transferMoney(req.session.user.id, req.body.to, req.body.amount); res.json({ status: 'ok' }); }); // Endpoint для токена в SPA app.get('/csrf', csrfProtection, (req, res) => { res.json({ token: req.csrfToken() }); }); ``` Щоб обійти цей захист, зловмиснику треба одночасно обійти обмеження SameSite cookie, вгадати CSRF токен і підробити заголовок Origin. На практиці подолати перші два вже нереалістично.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.