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 (например, <, >, &,
), на их безопасные аналоги (<, >, &, ", '). Это предотвращает интерпретацию кода браузером.
Реализация должна быть контекстно-зависимой. Для вывода в 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 для проверки.


