ПроКодинг - Откроем для вас мир IT!

Представьте ситуацию: пользователь вошел в свой банковский аккаунт на Bank.ru и открыл вкладку с котиками. На странице с котиками спрятан невидимый скрипт, который незаметно отправляет запрос на перевод денег со счета пользователя на счет злоумышленника. Браузер видит, что это запрос к домену Bank.ru, и автоматически подставляет куки сессии. Банк получает запрос, проверяет авторизацию по кукам, видит все в порядке и совершает транзакцию. Это классическая атака CSRF (Cross-Site Request Forgery), или межсайтовая подделка запроса. Звучит как магия? Нет, это просто дыра в логике обработки запросов, которую часто пропускают даже опытные бэкендеры.

Суть проблемы: почему браузер обманывается

Чтобы понять, как защититься, нужно осознать корень зла. HTTP - протокол stateless, он не хранит состояние между запросами. Но браузеры хранили куки и научились автоматически прикреплять их к запросам на тот же домен. Для браузера нет разницы, кто инициировал запрос: сам пользователь, кликнувший кнопку «Оплатить», или скрипт с другого сайта, выполнивший `fetch()` или отправивший форму POST. Если эндпоинт принимает действия только на основе наличия валидной сессии в куках, он слеп. Он не знает, был ли этот запрос намеренным действием пользователя или побочным эффектом чужого кода.

Раньше CSRF считалась менее опасной, чем XSS (межсайтовый скриптинг), потому что требовала предварительной аутентификации жертвы. Но сегодня, когда мы строим SPA (Single Page Applications) и микросервисные архитектуры, последствия могут быть катастрофическими. От смены email-адреса до вывода средств из криптокошелька. Разработчик обязан закладывать защиту на уровне фреймворка, а не надеяться на авось.

Старая школа: токены синхронизации

Классическое решение проблемы CSRF - использование уникальных токенов. Механизм работает так: при рендеринге формы сервер генерирует случайную строку (токен), привязанную к текущей сессии пользователя, и помещает её в скрытое поле ``. Когда пользователь отправляет форму, браузер возвращает этот токен обратно. Сервер сравнивает полученный токен с тем, что хранится в сессии. Если они совпадают - запрос легитимный. Если нет или токена нет вообще - атака.

Почему это работает? Потому что злоумышленник не может прочитать содержимое ответа вашего сайта из-за политики одинакового источника (Same-Origin Policy). Он может отправить запрос, но не знает значения токена, который находится внутри HTML-кода страницы. Без правильного токена сервер отклонит запрос.

Сравнение методов защиты от CSRF
Метод Надежность Сложность внедрения Поддержка браузерами
Anti-CSRF Token Высокая Средняя (требуется интеграция в шаблоны) Все современные
SameSite Cookie Attribute Высокая (с ограничениями) Низкая (настройка на уровне сервера) Chrome, Firefox, Safari (последние версии)
Custom Header Check Средняя Низкая (для AJAX) Все
Referer/Origin Check Низкая Низкая Все (но данные могут отсутствовать)

Современный стандарт: атрибут SameSite

В последние годы ситуация изменилась благодаря появлению атрибута SameSite для куки. Этот флаг позволяет браузеру контролировать, будут ли куки отправлены вместе с кросс-доменными запросами. У него есть три значения:

  • Lax (значение по умолчанию в Chrome): Куки отправляются только при GET-запросах, инициированных пользователем (например, клик по ссылке), но блокируются при POST-запросах с других сайтов. Это защищает от большинства CSRF-атак, так как вредоносные действия обычно выполняются через POST.
  • Strict: Куки не отправляются ни при каких кросс-доменных запросах. Даже если вы перейдете по ссылке с Facebook на ваш сайт, вы будете разлогинены, пока не обновите страницу. Максимальная безопасность, но плохой UX.
  • None: Куки отправляются всегда, как раньше. Требует обязательного использования HTTPS и флага Secure. Именно здесь кроется риск, если вы забудете про токены.

Хотя SameSite=Lax решает многие проблемы, полагаться только на него опасно. Не все браузеры исторически поддерживали его корректно, и некоторые старые версии могут игнорировать флаг. Кроме того, если ваше приложение использует AJAX-запросы с методом POST, SameSite=Lax всё равно заблокирует куки, что может сломать функциональность, если вы ожидаете автоматической авторизации. Поэтому лучший подход - комбинированный: используйте SameSite=Strict или Lax как первый барьер, а токены как второй.

Концептуализация атрибута SameSite как защитного щита для куки от кросс-доменных запросов.

Особые случаи: API и мобильные приложения

Здесь начинается путаница. Многие думают: «У нас REST API, мы используем JWT в заголовке Authorization, значит, CSRF нам не грозит». И они правы... частично. Атаки CSRF работают только тогда, когда браузер автоматически подставляет механизм аутентификации (обычно куки).

Если ваш клиент (веб-браузер или мобильное приложение) явно передает токен доступа в заголовке `Authorization: Bearer ...`, браузер не может сделать это автоматически для стороннего сайта. Скрипт с evil.com не сможет добавить этот заголовок в запрос к вашему API без явного указания CORS (Cross-Origin Resource Sharing). А если вы настроили CORS правильно, ограничив доступ только своим доменам, то проблема решается сама собой.

Однако будьте осторожны с гибридными архитектурами. Если у вас есть часть приложения, которая использует куки для сессий (например, админка на PHP), а другая часть - SPA на React с JWT, убедитесь, что эндпоинты, работающие с куками, защищены токенами или флагом SameSite. Частая ошибка: разработчики добавляют JWT-авторизацию, но оставляют старые эндпоинты, принимающие куки, незащищенными.

Проверка заголовков Origin и Referer

Еще один метод - проверка заголовков `Origin` или `Referer`. При кросс-доменных запросах браузер добавляет эти заголовки, указывающие источник запроса. Сервер может проверить, входит ли домен из этих заголовков в белый список разрешенных источников.

Этот метод имеет недостатки. Заголовок `Referer` может отсутствовать из-за настроек конфиденциальности браузера или политик передачи реферера. Заголовок `Origin` более надежен, но также может быть пустым в некоторых случаях (например, при навигации из HTTPS в HTTP, хотя сейчас это редкость). Использовать эту проверку можно как дополнительный слой защиты, но не как единственный. Хакеры могут использовать техники спуфинга или находить способы обойти проверки в специфических конфигурациях веб-серверов.

Изометрическая иллюстрация рабочего места разработчика с акцентом на защиту API и заголовки.

Чек-лист для безопасного внедрения

Чтобы спать спокойно, пройдитесь по этому списку перед релизом:

  1. Используйте HTTPS везде. Без шифрования куки могут быть перехвачены, а атаки усложняются, но становятся возможными.
  2. Установите флаг Secure для всех сессионных куки. Это гарантирует, что они не будут отправлены по незащищенному соединению.
  3. Настройте атрибут SameSite. Выберите `Lax` для общего случая или `Strict`, если ваша бизнес-логика позволяет агрессивный выход из сессии при внешних переходах.
  4. Интегрируйте CSRF-токены. Большинство современных фреймворков (Django, Spring Security, Laravel, Express.js с middleware) делают это «из коробки». Убедитесь, что эта функция не отключена ради удобства разработки.
  5. Для AJAX-запросов используйте кастомные заголовки. Например, `X-Requested-With: XMLHttpRequest`. Хотя IE11 больше не поддерживает автодобавление этого заголовка, вы можете добавить его вручную в fetch/XHR. Сервер должен проверять наличие этого заголовка и отклонять запросы без него для чувствительных операций.
  6. Тестируйте на разных браузерах. Поведение SameSite немного отличается в Safari и старых версиях Firefox. Используйте инструменты разработчика, чтобы увидеть, какие куки реально отправляются.

Типичные ошибки новичков

Первая ошибка - генерация токена на стороне клиента (JavaScript) и хранение его в LocalStorage. Такой токен доступен любому скрипту на странице, включая вредоносный XSS-скрипт. Токен должен генерироваться сервером и передаваться в HTML или защищенной куке, недоступной для JS (HttpOnly).

Вторая ошибка - использование одного и того же токена для всех форм на сайте. Если токен утечет (например, через историю URL или логирование), злоумышленник сможет подделать любые действия. Идеально - ротация токенов или привязка токена к конкретному действию (action-specific tokens).

Третья ошибка - игнорирование метода HTTP. Некоторые разработчики защищают только POST-запросы, забывая, что GET-запросы тоже могут изменять состояние сервера (например, `/delete?id=5`). По REST-конвенциям GET должен быть идемпотентным и безопасным, но legacy-код часто нарушает это правило. Лучше всего запретить изменение состояния через GET полностью.

Нужен ли CSRF-токен, если я использую JWT?

Не обязательно, если JWT передается исключительно в заголовке Authorization и не хранится в куках. Браузер не будет автоматически добавлять заголовок Authorization к запросам с других доменов. Однако, если вы храните JWT в куках (что иногда делают для SSR-рендеринга), то защита от CSRF становится необходимой, так как браузер будет автоматически подставлять эти куки.

Как проверить, включена ли защита от CSRF в Django?

В настройках проекта (`settings.py`) найдите параметр `MIDDLEWARE`. Убедитесь, что там присутствует `'django.middleware.csrf.CsrfViewMiddleware'`. Также убедитесь, что в ваших HTML-шаблонах форм используется тег `{% csrf_token %}`. Если вы используете APIView или DRF, защита может обрабатываться иначе (через аутентификационные классы), поэтому проверьте документацию вашего фреймворка.

Что делать, если SameSite=None ломает мои куки?

Если вы установили `SameSite=None`, вы обязаны также установить флаг `Secure`. Это означает, что куки будут работать только по протоколу HTTPS. Если вы тестируете локально на `http://localhost`, браузеры могут игнорировать такие куки. Решения: либо используйте `SameSite=Lax` для разработки, либо настройте локальный HTTPS (например, через mkcert), либо временно исключите проверку в dev-среде.

Можно ли защитить от CSRF только через CORS?

Нет. CORS контролирует, может ли JavaScript читать ответ сервера, но не запрещает браузеру отправлять запрос с куками. Простой запрос (simple request) с куками будет выполнен браузером, даже если политика CORS запрещает чтение ответа. Результат операции на сервере применится, но скрипт не увидит ответ. Поэтому для изменения данных (POST/PUT/DELETE) одной CORS недостаточно.

Как тестировать уязвимость к CSRF?

Создайте простую HTML-страницу на другом порту или домене. Добавьте туда форму с методом POST, действующую на нужный вам эндпоинт вашего приложения. В форме должны быть поля, соответствующие ожидаемым данным. Если после загрузки этой страницы действие на вашем основном сайте произошло (например, изменился профиль), значит, защита отсутствует или настроена неверно. Специализированные сканеры вроде OWASP ZAP или Burp Suite также имеют модули для проверки CSRF.