Блог · Кейс · XSS

Кейс из практики: отражённый XSS в поле промокода e-commerce проекта, где меньше всего ждали уязвимость.

2026-10-05 · 3 мин чтения

XSS в поле промокода, которое никто не считал опасным

Поле для ввода промокода обычно последнее место, где кто-то ждёт уязвимость. Это не форма регистрации, не поле поиска, не комментарии, где все уже привыкли фильтровать ввод. Просто текстовое поле на странице корзины, куда вбивают буквы и цифры и жмут «Применить». Именно поэтому там и обнаружилась дыра, которую я нашёл на одном из e-commerce проектов, где отвечал за backend довольно долго.

Код промокода проходил проверку на существование и срок действия, это была нормальная бизнес-логика, и с этой стороны всё работало как надо. Но значение из поля, после того как промокод применялся, подставлялось обратно на страницу в блок с сообщением об успехе, что-то вроде «Промокод ABC123 применён». И вот это сообщение рендерилось без экранирования спецсимволов.

Разница между «буквами и цифрами» и «произвольным текстом» здесь ключевая. Формально поле ничего не ограничивало на уровне интерфейса, валидация по маске стояла только на бэкенде, да и то проверялся сам факт существования промокода в базе, а не формат введённой строки до этого. Если промокод не находился, ошибка всё равно показывала исходное значение поля обратно пользователю, тем же необработанным способом. Достаточно было вставить в поле что-то вроде тега script с небольшим полезным содержимым внутри, и при показе страницы с ошибкой браузер честно его выполнял.

Хранимого XSS тут не было, это был отражённый вариант, то есть атака работала только если жертва сама перешла по специально сформированной ссылке с вредоносным промокодом в параметрах, а не через постоянно хранящуюся где-то payload. Это снижает масштаб, но не отменяет проблему: отражённый XSS прекрасно работает в фишинговых рассылках, где ссылка выглядит как обычная ссылка на знакомый магазин с промокодом на скидку, а по факту ведёт на выполнение чужого скрипта в контексте вашего сайта с вашей сессией пользователя.

Исправление заняло немного времени, экранирование вывода на уровне шаблона, плюс нормальная валидация формата промокода на входе, чтобы подобные символы вообще не долетали до логики показа ошибки. Самым полезным результатом стало не закрытие именно этой дыры, а то, что после неё я стал по-другому смотреть на весь список мест, где пользовательский ввод появляется на странице повторно. Таких мест в любом сколько-нибудь крупном проекте набирается заметно больше, чем кажется на первый взгляд, если не искать целенаправленно.

Если в вашем проекте давно не проверяли, что происходит с пользовательским вводом после того, как он один раз прошёл валидацию на входе, это обычно хорошее место для начала ручного аудита кода. Автоматические сканеры такие вещи иногда находят, а иногда нет, особенно если payload должен пройти через два разных шаблона прежде чем отрендериться.

Именно такие места я ищу на ручном аудите кода, не полагаясь только на автоматические сканеры.