Stored XSS возникает, когда вредоносный код сохраняется в базе данных и выполняется при повторном выводе пользователем. В отличие от отражённого XSS, атака не требует прямого взаимодействия жертвы с злоумышленником; достаточно одного входа на сайт, где уже лежит заражённый контент.

Суть угрозы: как работает механизм хранения XSS-скриптов в формате текста

Механизм базируется на разрыве между хранением данных и их отображением. Пользователь отправляет строку с кодом, сервер сохраняет её в БД без изменений. При последующем запросе другой страницы система извлекает запись и передаёт её в шаблон вывода.

Ключевая ошибка — использование методов вывода, которые интерпретируют содержимое как HTML-разметку. Например, функция echo в PHP или innerHTML в JavaScript без предварительной обработки превращает текст в исполняемый код.

В контексте пользовательского форматирования (например, редакторы с поддержкой Markdown или Rich Text) риск возрастает, если система не различает границы между контентом и инструкциями по отрисовке. Злоумышленник может внедрить тег <img> с событием onerror=alert(1) или использовать события на уровне DOM.

Критерии оценки риска: когда пользовательский ввод требует санитизации

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

Риск критический, если вывод происходит внутри тега <script>, <iframe> или через атрибуты событий (onclick, onload). Даже один символ «<» в поле ввода может привести к выполнению кода.

Оценка риска также учитывает контекст вывода. Если данные используются только в текстовом блоке div, риск ниже, но не исчезает полностью из-за возможности внедрения через атрибуты или CSS-свойства.

Шаг 1. Валидация формата: определение границ допустимого контента

Первая линия обороны — валидация на стороне клиента и сервера. На сервере проверяется соответствие данных строгому формату (например, только буквы, цифры и пробелы для простых полей). Для сложных полей с форматированием используется whitelist-подход: разрешены только теги из заранее утверждённого списка.

Пример реализации: создание регулярного выражения или парсера, который пропускает только <b>, <i>, <u> и их закрывающие аналоги. Попытка внедрить <script> или <img> блокируется на этапе сохранения.

Важно не полагаться только на клиентскую валидацию. Пользователь может отключить JavaScript, а злоумышленник — обходить проверки через API-запросы.

Шаг 2. Эскейпинг спецсимволов: защита от выполнения кода в контексте вывода

Эскейпинг — замена символов, имеющих особое значение в HTML (например, <, >, &,

), на их безопасные аналоги (&lt;, &gt;, &amp;, &quot;, &apos;). Это предотвращает интерпретацию кода браузером.

Реализация должна быть контекстно-зависимой. Для вывода в HTML-атрибуте достаточно экранировать кавычки и угловые скобки, но для атрибута с одинарными кавычками нужно экранировать и двойные, и обратную кавычку.

Используйте готовые библиотеки или встроенные функции языков (например, htmlspecialchars в PHP), которые минимизируют риск человеческой ошибки.

Шаг 3. Контекстно-зависимая санитизация: разные правила для HTML, JS и URL

Одна и та же строка требует разных обработок в зависимости от места вывода. В HTML-контексте экранируются все спецсимволы. В JavaScript-контексте (например, внутри тега <script>) нужно экранировать кавычки и обратные слеши.

Для URL-параметров применяется кодирование по стандарту URI, где пробелы заменяются на %20, а спецсимволы — на их процентное представление.

Ошибка: применение универсального эскейпа ко всему контенту. Это может сломать валидность HTML или нарушить работу скриптов. Правильный подход — выбор метода обработки под конкретный контекст.

Шаг 4. Хранение данных: принципы безопасного сохранения в БД

База данных не должна хранить данные в формате, готовом к выполнению кода. Сохраняйте только «чистый» текст, без HTML-разметки или скриптов.

Если форматирование необходимо, используйте отдельное поле для стилей (например, CSS-классы) и храните разметку отдельно от контента. Это упрощает санитизацию при выводе.

Избегайте хранения данных в формате JSON без экранирования, если они будут выводиться как текст. Используйте сериализацию с правильным кодированием символов.

Шаг 5. Вывод контента: применение правильных тегов и атрибутов

При генерации HTML используйте только безопасные теги и атрибуты. Избегайте использования innerHTML без предварительной обработки.

Для динамического добавления контента через DOM-интерфейсы применяйте методы, которые создают узлы из текста (textContent), а не из HTML-строки (innerHTML).

Если вывод в атрибуте, убедитесь, что значение экранировано и заключено в кавычки соответствующего типа.

Шаг 6. Обработка ошибок: предотвращение утечек информации через сообщения об ошибках

Сообщения об ошибках не должны содержать сырые данные из БД или запросов, так как это может раскрыть структуру приложения.

Используйте шаблонные сообщения с общими формулировками («Неверный формат данных», «Ошибка обработки запроса»).

Логирование должно происходить в защищённом месте, без записи чувствительных данных или сырых запросов.

Шаг 7. Мониторинг и аудит: регулярная проверка уязвимостей в коде

Регулярно проводите статический анализ кода (SAST) для поиска мест, где данные выводятся без обработки.

Используйте инструменты динамического анализа (DAST) для симуляции атак XSS и проверки эффективности защитных механизмов.

Внедряйте автоматизированные тесты в CI/CD-пайплайн, которые проверяют новые изменения на предмет уязвимостей.

Вопросы и ответы

Можно ли полностью запретить HTML-разметку в полях ввода?

Да, это самый безопасный вариант для простых полей. Однако для сложных форматов (например, редакторов с поддержкой жирного текста или списков) полный запрет нецелесообразен. В таких случаях используется whitelist-подход с разрешением только безопасных тегов.

Достаточно ли экранировать данные только на стороне клиента?

Нет. Клиентская валидация легко обходится. Санитизация должна происходить на сервере перед сохранением и перед каждым выводом данных.

Как проверить, что санитизация работает корректно?

Проведите тесты с внедрением стандартных векторов атак (например, <script>alert(1)</script>) и убедитесь, что они выводятся как текст, а не выполняются. Используйте инструменты вроде XSS-Payload-Generator для проверки.

По теме