CSRF: как перестать быть пешкой в чужой игре
Введение: почему браузер — это не ваш друг
Представьте: пользователь заходит на сайт банка, авторизуется, а потом случайно открывает заранее подготовленную злоумышленником страницу. Браузер, будучи добросовестным помощником, автоматически отправляет все куки, включая сессионные, с каждым запросом — даже если пользователь ничего не нажимал. Вот тут и происходит CSRF: атакующий отправляет POST-запрос на /transfer, а браузер жертвы его выполняет, не спрашивая разрешения.
Это не гипотетический сценарий. В 2021 году через CSRF было украдено $10 млн с помощью фишинговых писем, где жертвы кликали по ссылкам, которые автоматически отправляли транзакции. А в 2023-м исследователи показали, как через CSRF можно было обойти двухфакторную аутентификацию в некоторых сервисах.
Проблема в том, что браузеры не различают "свой" и "чужой" код — они просто выполняют все запросы, которые приходят от открытых страниц. А серверы часто не проверяют, откуда пришёл запрос — только то, что он пришёл с валидными куками.
Как это работает: механика атаки
CSRF атакуют три ключевых свойства веба:
- Автоматическая отправка кук — браузер всегда шлёт куки домена, с которого пришёл запрос.
- Метод
GETпо умолчанию — ссылки открываются без подтверждения, аGET-запросы часто считаются безопасными. - Cross-Origin Requests — если жертва авторизована на
bank.com, а атакующий контролируетevil.com, он может заставить браузер отправить запрос наbank.comс куками жертвы.
Пример атаки: изменение пароля
Предположим, у вас есть эндпоинт /change-password с параметрами в GET-запросе:
https://example.com/change-password?email=user@example.com&new_password=hacked123
Злоумышленник создаёт страницу на evil.com с такой ссылкой:
<a href="https://example.com/change-password?email=user@example.com&new_password=hacked123" target="_blank">Нажми, чтобы выиграть приз!</a>
Жертва кликает — и её пароль меняется. Без защиты это работает всегда, если пользователь авторизован.
Защита: что работает, а что — нет
❌ Ложные решения (не защищают от CSRF)
- Проверка
Referer— легко обойти черезReferer-спуфинг или прокси.# Плохой пример (Django) if request.META.get('HTTP_REFERER') != 'https://yourdomain.com': return HttpResponseForbidden() - CSRF-токены в
GET-параметрах — токен можно угадать или подсмотреть в URL. - Блокировка
GET-запросов для изменяющих данные эндпоинтов — не всегда возможно (например, дляGET /api/webhooks).
✅ Рабочие методы
1. CSRF-токены в POST-формах
Самый распространённый способ — генерировать уникальный токен для каждой сессии и проверять его в POST-запросах.
Пример для Django (средствами фреймворка):
# settings.py
MIDDLEWARE = [
'django.middleware.csrf.CsrfViewMiddleware',
# ...
]
# В шаблоне (template.html):
<form method="post">
{% csrf_token %}
<input type="submit" value="Отправить">
</form>
Как это работает:
- При рендере формы Django добавляет скрытое поле с токеном:
<input type="hidden" name="csrfmiddlewaretoken" value="u32h9fj32hf93j2hf93jfh23"> - Сервер проверяет, что токен из запроса совпадает с токеном в сессии.
Ограничения:
- Работает только для
POST-форм. Для AJAX нужно отправлять токен в заголовкеX-CSRFToken. - Не защищает от
GET-атак (см. ниже).
2. SameSite-куки
Модернизированный подход — ограничить куки, чтобы браузер не отправлял их с других сайтов.
Конфигурация для Nginx + Django:
location / {
add_header Set-Cookie "sessionid=...; SameSite=Strict; Secure";
}
Параметры SameSite:
Strict— куки отправляются только для запросов с того же сайта.Lax— куки отправляются для "безопасных" навигационныхGET-запросов (например, при клике по ссылке).None— куки отправляются всегда, но требуетSecure(HTTPS).
Пример для Node.js (Express):
res.cookie('session', 'abc123', {
sameSite: 'strict',
secure: true,
httpOnly: true
});
Эффективность:
- Защищает от ~90% CSRF-атак, если настроен правильно.
- Не требует изменений в коде — работает на уровне браузера.
3. Double Submit Cookie
Комбинация токена в куках и в теле запроса. Если они не совпадают — запрос отклоняется.
Пример для Express (Node.js):
const csrf = require('csurf');
const cookieParser = require('cookie-parser');
app.use(cookieParser());
app.use(csrf({ cookie: { httpOnly: false } }));
// В шаблоне:
<form method="post">
<input type="hidden" name="_csrf" value="<%= req.csrfToken() %>">
<input type="submit">
</form>
Как это работает:
- Сервер генерирует токен и сохраняет его в куки (
XSRF-TOKEN). - Клиент отправляет тот же токен в теле запроса (
_csrf). - Сервер сравнивает оба значения.
Плюсы:
- Защищает от
GET-атак (если токен не передаётся в URL). - Работает с AJAX, если правильно настроить заголовки.
4. Специфичные заголовки
Для API можно требовать наличие заголовка X-Requested-With: XMLHttpRequest или пользовательского заголовка, который клиент должен знать.
Пример для Flask:
from flask import request, abort
@app.route('/api/transfer', methods=['POST'])
def transfer():
if request.headers.get('X-Custom-Auth') != 'expected-value':
abort(403)
# Обработка запроса
Ограничения:
- Работает только для API, где клиент контролируется (например, мобильное приложение).
- Легко обойти, если заголовок угадывается.
Типичные ошибки и как их избежать
Забыв защитить
GET-эндпоинты с побочными эффектами- Плохо:
/withdraw?amount=1000(даже если этоGET, деньги уйдут). - Решение: Использовать
POSTили добавлять CSRF-защиту дляGET-запросов с токеном в куках.
- Плохо:
Использовать
RefererвместоSameSite- Проблема:
Refererлегко подделать через прокси или модификацию заголовков. - Решение: Перейти на
SameSite=Strict+Secure.
- Проблема:
Не проверять токен в AJAX-запросах
- Плохо: Отправлять только куки, забывая про токен в заголовке.
- Решение: Автоматически добавлять токен в заголовок
X-CSRF-Token:// jQuery пример $.ajaxSetup({ headers: { 'X-CSRFToken': $('input[name="csrfmiddlewaretoken"]').val() } });
Хранить токены в локальном хранилище (
localStorage)- Проблема: JavaScript-код может украсть токен из
localStorage. - Решение: Хранить токен только в
HttpOnly-куках.
- Проблема: JavaScript-код может украсть токен из
Игнорировать
GET-параметры в чувствительных эндпоинтах- Пример уязвимости:
# Django view (уязвим!) @csrf_exempt # ❌ Опасно! def sensitive_action(request): if request.method == 'GET': return do_something_bad(request.GET['param']) - Решение: Всегда использовать
POSTдля изменяющих данных эндпоинтов.
- Пример уязвимости:
Практические шаги для защиты проекта
1. Аудит уязвимых эндпоинтов
Пройдитесь по коду и найдите:
- Все
GET-запросы, которые изменяют состояние (например,/delete,/transfer). - Формы без CSRF-защиты.
- API-эндпоинты без проверки заголовков.
Команда для поиска в Django:
grep -r "csrf_exempt" . # Найдёт все места, где отключена защита
grep -r "method=['\"]GET" . # Найдёт GET-запросы
2. Настройка SameSite для кук
Обновите конфигурацию сервера и фреймворка:
- Nginx:
proxy_cookie_flags ~^(.*?)(;.*?)*$ $1;SameSite=Strict;Secure; - Django:
SESSION_COOKIE_SAMESITE = 'Strict' CSRF_COOKIE_SAMESITE = 'Strict' - Express:
res.cookie('session', '...', { sameSite: 'strict', secure: true });
3. Тестирование защиты
Используйте инструменты для проверки:
- Burp Suite — для ручного тестирования CSRF-атак.
- OWASP ZAP — для автоматического сканирования.
- CSRF-токен тестер:
Если ответ —curl -v -b "sessionid=..." -H "Referer: http://evil.com" http://your-site.com/transfer403 Forbidden, защита работает.
4. Документирование и обучение команды
- Добавьте checklist для ревью кода:
- Все формы имеют CSRF-токен.
- Чувствительные
GET-эндпоинты защищены. - Куки настроены с
SameSite=Strict.
- Проведите обучение по безопасности для backend-разработчиков.
Заключение: CSRF — это не баг, а фича безопасности
CSRF — это не теоретическая угроза, а реальная проблема, которая может привести к краже данных, финансовым потерям или репутационным рискам. Защита от неё должна быть столь же рутинной, как и валидация входных данных.
Что делать прямо сейчас:
- Для новых проектов: Включите CSRF-защиту по умолчанию (Django, Flask-WTF, Express-CSRF).
- Для старых проектов: Проведите аудит и закройте уязвимости за 2 недели.
- Для API: Используйте
SameSite-куки + специфичные заголовки. - Для фронтенда: Автоматизируйте добавление CSRF-токенов в AJAX-запросы.
Помните: CSRF-атака работает, только если у вас есть уязвимые эндпоинты. Закройте их — и проблема исчезнет.