XSS через чат поддержки: как атакующий пытался украсть сессию администратора

Кейс из практики. Проект — финансовый онлайн-сервис с личными кабинетами, приёмом платежей, чатом поддержки и админ-панелью. Мы ведём его безопасность и инфраструктуру. Название проекта, адреса и даты не раскрываем; технические детали обобщены настолько, чтобы их нельзя было использовать для атаки.
Утром в чат поддержки начали приходить сообщения, похожие на вопросы клиента, но с HTML-вставкой. Расчёт был не на сайт, а на человека: если браузер оператора выполнит вставку в админке, данные его сессии уйдут на внешний сервер.
Что произошло
С одного адреса за утро открылось несколько сессий чата, в каждой — одинаковые сообщения со вставкой. Параллельно тот же адрес регистрировал аккаунты, запрашивал сброс пароля и пробовал войти в админку. Это типичная картина разведки: атакующий одновременно проверяет несколько входов и смотрит, какой откроется.
Чем опасен отложенный XSS
Обычный XSS срабатывает сразу на странице, где его оставили. Отложенный (blind XSS) сохраняется в базе и срабатывает позже в другом интерфейсе — админке, CRM, панели модерации, которые атакующий сам не видит.
| Канал ввода | Где выводится | Кто под угрозой |
|---|---|---|
| Чат поддержки | Панель оператора | Сотрудник поддержки |
| Форма обращения, тикет | Админка, письмо-уведомление | Администратор |
| Имя, адрес доставки | Список клиентов, экспорт | Менеджер |
| Заголовки запроса | Просмотр логов в браузере | Разработчик |
Если скрипт выполнится в админке, атакующий может получить сессию администратора или выполнять действия от его имени прямо из его браузера.
Почему вставка не сработала
- Интерфейсы выводят текст, а не HTML. Сообщения рендерятся через React как строка; вставка пользовательского текста как HTML нигде не используется. Оператор увидел теги буквально, как символы.
- Cookie админки помечены httpOnly. Даже при выполнении скрипта он не смог бы прочитать токен сессии.
- Уведомления экранируются. Текст, который уходит сотрудникам в мессенджер, проходит экранирование спецсимволов, поэтому разметка не интерпретируется и там.
Что мы добавили сверху
- Отказ на входе. API чата отвечает 400 на сообщения с HTML-тегами, атрибутами-обработчиками и опасными схемами ссылок. Обычным клиентам писать разметку в поддержку незачем.
- Блокировка сессий. Сессии чата атакующего помечены как заблокированные и закрыты; API отвечает 403, если сессия заблокирована или открыта с адреса из чёрного списка. Переписку сохранили как доказательство.
- Чёрный список в двух местах. Адреса внесены и в правила веб-сервера, и в список, который проверяет само приложение.
- Лимиты частоты. На создание сессии и отправку сообщения действует отдельный лимит, чтобы чат нельзя было использовать для массовой рассылки.
Проверьте свой чат и формы
- Составьте список всех мест, где выводится пользовательский текст: админка, CRM, письма, уведомления, экспорт.
- Поищите в коде вывод HTML без экранирования (
innerHTML,dangerouslySetInnerHTML,v-html,|safe) и убедитесь, что туда не попадают данные клиентов. - Проверьте флаги cookie админки:
HttpOnly,Secure,SameSite. - Включите Content Security Policy хотя бы для админ-панели: запрет внешних адресов в
connect-srcиimg-srcобрезает канал утечки. - Отправьте в свой чат
<b>тест</b>: если оператор видит жирный текст, нужен срочный аудит.
Материалы для проверки
Если нужна реализация
Состав работ зависит от текущего проекта и подтверждённой причины. Можно обсудить диагностику или разработку подходящего решения.
Вопросы и ответы
Чем blind XSS отличается от обычного XSS?
Обычный XSS срабатывает сразу на странице, где его вставили. Blind XSS сохраняется в базе и выполняется позже в другом интерфейсе — чаще всего в админке или CRM.
Достаточно ли React, чтобы защититься от XSS?
React экранирует строки по умолчанию, но защита пропадает при явной вставке HTML, ссылках с опасными схемами и в сторонних виджетах. Нужны ещё httpOnly-cookie и CSP.
Зачем отклонять HTML на входе, если вывод и так безопасен?
Это второй рубеж: запись не попадает в базу, и будущая ошибка в выводе не превратится в уязвимость. Отказы на входе к тому же видны в логах как попытки атаки.