Бот, которого не было в вашей группе, мог спрятать код в сообщении, ждать месяцами и сработать, когда кто‑то экспортирует переписку. Telegram исправил баг молча.
Сначала мы сообщили о баге вендору и молчали до выхода фикса. Тестировали только на своих аккаунтах. Это раскрытие после выхода патча.
Мы нашли хранимую XSS в Telegram Desktop и 3 июня 2026 года сообщили о ней. В июле Telegram выпустил фикс — и отказался одобрить нашу публикацию, хотя проблема уже была исправлена. Мы отказались от вознаграждения и попросили отдать эти деньги на благотворительность; мы также просили согласовать дату публикации. Этот текст — и есть раскрытие. Кода эксплойта здесь нет, живые системы мы не атаковали.
Утро понедельника
Давайте я опишу сцену.
9:47 утра. Финтех‑компания сдаёт инженерный чат комплаенсу — проверка дважды в год, рутина. Ведущий разработчик открывает Telegram Desktop, нажимает «Экспорт истории чата», выбирает HTML и открывает файл в Chrome.
Страница загружается: сообщения, отметки времени, имена, фрагменты кода, ветка, где кто‑то вставил AWS‑ключи «буквально на секунду».
Он не видит тег <script>, который уже сработал внутри кнопки. За четыреста миллисекунд, пока страница отрисовывалась, каждое сообщение на ней упаковали в JSON и отправили на сервер в другой стране.
Он видит обычный экспорт. Атакующий видит всё.
Сообщение с кодом пришло семь месяцев назад. Новый коллега переслал его, думая, что это ссылка на корпоративный блог. Бот, который его отправил, никогда не был в группе. Ему и не надо было.
Эта сцена — собирательная. Цепочку атаки из неё — пересылку, экспорт, открытие и утечку — мы показали в тестах.
Коротко
Telegram Desktop умеет выгружать чат в HTML‑файл, а этот файл открывается в браузере. Мы выяснили, что бот может спрятать невидимый JavaScript в inline‑кнопке — и этот код сработает, как только кто‑то откроет экспорт с такой кнопкой. Без второго клика. Без предупреждений.
Из открытой страницы скрипт может прочитать все сообщения, отправить их на сервер атакующего и переписать саму страницу.
Бот при этом никогда не вступает в чат. Достаточно одного пересланного сообщения.

Исправлено в Beta v6.9.4 / Stable v7.0.1. На момент публикации: advisory от Telegram нет, CVE не присвоен.
Одна пропущенная строка
Код экспорта живёт в файле export_output_html.cpp. Текст сообщений перед попаданием на страницу экранирует: функция SerializeString() превращает <, >, &, ", ' в безопасные HTML‑сущности, заменяет переводы строк и Unicode‑разделители на <br> и hex‑кодирует управляющие ASCII‑символы. Та же функция применяется к именам отправителей и другим полям.»
А к тексту кнопок её не применяли. Строка 1752 просто записывала его на страницу:
button.text.toUtf8()
Так любой HTML в поле text кнопки становился живой разметкой в экспортированной странице. Включая теги <script>.
Эта строка пришла из коммита 52c779bf в начале 2024 года и уехала в стабильный релиз v4.15.1 8 марта 2024-го. В продакшене она прожила примерно два года и четыре месяца.
Почему это опаснее обычного XSS
XSS — это когда данные на странице начинают интерпретироваться как код. «Хранимая» — когда вредоносные данные где‑то сохраняются и срабатывают позже. Обычно для этого атакующему нужен доступ к цели. Здесь — нет:
1. Доступ не нужен. Бота можно создать за пару минут. Bot API позволяет прикреплять к сообщениям inline‑кнопки, а поле text принимает любой Unicode — включая HTML‑теги. Клавиатура, состоящая только из URL‑кнопок, переживает пересылку (правило CopyMarkupToForward). Поэтому коллега или любой участник может принести отравленное сообщение в группу, которой бот никогда не касался. (По тому же правилу можно пересылать игры и некоторые SwitchInline‑кнопки. Нам они не понадобились.)
2. Он ждёт. Код лежит в истории сообщений спящим текстом. Он ничего не делает, пока кто‑то не экспортирует чат.
3. Одно сообщение доходит до многих. Одна пересылка в супергруппу на 200 000 человек может задеть каждого, чей экспорт включает это сообщение и кто откроет файл.
Как устроена атака. Часть 1: пересланное сообщение
Атакующий создаёт бота. Бот отправляет одно сообщение: текст — «посмотрите наш новый пост в блоге», в text кнопки — тридцать невидимых символов U+3164 и следом тег <script>, а URL кнопки ведёт на правдоподобный блог.
В Telegram кнопка выглядит пустой или почти пустой. Она при этом работает как ссылка, так что ничего не вызывает подозрений.

Кто‑то пересылает это сообщение в целевой чат. Telegram сохраняет кнопку, потому что она URL‑типа. Бот никогда не был участником группы. Через API у него ноль доступа к её сообщениям.


Семь месяцев спустя кто‑то экспортирует чат.

Видео ниже записано до выхода фикса. На нём вся цепочка целиком: страница превращается в фейковую форму верификации Telegram, а каждое сообщение на ней тихо уходит на сервер атакующего.
Как устроена атака. Часть 2: бот без доступа
Вот это нам показалось самым интересным.
В Telegram есть privacy mode для ботов. По умолчанию бот в группе не получает обычные сообщения и историю — только команды, ответы, сообщения, отправленные через бота, и служебные сообщения. Администраторы видят на боте пометку: «не имеет доступа к сообщениям». Поэтому группы спокойно добавляют служебных ботов: погода, опросы, напоминания.

HTML‑экспорт ломает эту границу. Когда вы экспортируете чат, Telegram записывает в файл и собственные сообщения бота — вместе с кнопками. Скрипт бота оказывается на странице и оттуда читает каждое показанное на ней сообщение. API никогда не отдавал боту эти данные. Их отдал файл экспорта.
Бот погоды с privacy mode не может читать ваши сообщения. Пока кто‑то не откроет экспорт с его кнопкой. Как только экспорт будет открыт в браузере — сообщения улетят атакующему.
CVSS называет это Scope Changed: изначально уязвимость — в локальном коде экспорта Telegram Desktop, а ущерб наступает в браузере, в другой среде.
Что забирает скрипт
Скрипт исполняется сразу при открытии файла:
— Сообщения. Он читает DOM страницы (.message.default): каждое показанное сообщение с отправителем и временем.
— Данные чата. Из шапки страницы: название, тип, число участников.
— Всё остальное на странице. document.body.innerText. В нашем PoC стоит лимит 200 КБ — это тестовое решение, а не граница импакта.
— Путь к файлу. location.href заодно раскрывает имя пользователя ОС и структуру папок.

Ещё наш PoC показал, что страницу можно заменить целиком — на фейковую форму «Необходима верификация» с брендингом Telegram. В демо поля пароля были read‑only, и на 127.0.0.1 уходил только тестовый email. Это была наша страховка, а не ограничение техники: тот же контроль над страницей можно использовать для фишинга.
Есть и другое применения описанному вектору. Скрипт может отредактировать то, что вы видите: изменить даты, имена отправителей, текст сообщений, спрятать, переставить или вставить сообщения, подменить контекст. История на серверах Telegram и файл на диске остаются нетронутыми. Но если экспорт чата используется как доказательство — в проверке или в суде, — тому, что на экране, верить уже нельзя.
Одно сообщение — много экспортов

Пересылка — это то, что даёт масштаб. В группах с отключённой защитой контента сообщение может переслать любой участник: публичная группа на 10 000 человек, инженерный чат на 2 400, трейдинговая группа на 200 000 — каждая копия несёт свой код. Каналы — отдельная история: чтобы там что‑то опубликовать, нужны права администратора или свой человек, так что это таргетированный вектор, а не бесплатное массовое распространение. И каждая отравленная страница экспорта срабатывает сама по себе; страница без кода ничего не делает.
Код живёт в истории до удаления сообщения. Те, кто вступил позже, тоже могут его получить — если им видна предыдущая история и сообщение попадает в диапазон их экспорта.
И в отличие от вложения, это сложно сигнатурно затектить. Это просто текст в базе Telegram. URL скрипта можно заблокировать, а само сообщение — нет. Оно превращается в код только тогда, когда происходит не связанное с ним обычное действие — экспорт чата. Между отправкой и срабатыванием могут пройти месяцы или годы. Атакующий оставляет сообщение и ждёт.
Фикс
Фикс — это один вызов функции, и он содержится в коммите 8457d13a, “Fix escaping in HTML export of keyboards” (John Preston; написан 30 июня, закоммичен 2 июля 2026):
- block.append(button.text.toUtf8()); + block.append(SerializeString(button.text.toUtf8())); - ? ("return ShowTextCopied('" + content + "');").toUtf8() + ? ("return ShowTextCopied('" + + QString(content) + .replace('\\', u"\\\\"_q) + .replace('\'', u"\\'"_q) + + "');").toUtf8()
Тем же коммитом закрыт сопутствующий вектор: инъекция JavaScript в атрибут onclick="return ShowTextCopied('…')" у кнопок копирования. Теперь content экранирует обратные слэши и одинарные кавычки, прежде чем стать частью JS‑строки.
Насколько это серьёзно? CVSS 3.1 даёт 8.2 (High): CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:N. Мы сознательно выбрали Attack Vector Local, а не Network: уязвимая часть — локальный код экспорта, а сетевой путь доставки кода обрабатывает данные корректно. Если поменять эту метрику, получилось бы 9.3, но нам важнее оценка, которую можно защитить. Ущерб от этого не меняется: чувствительные данные уходят из открытого документа, а запись можно переписать.
Если вы пользуетесь Telegram Desktop: обновитесь. Первый релиз с фиксом — Beta v6.9.4 (3 июля 2026), затем стабильная версия v7.0.1 (14 июля 2026). Обновление не перезаписывает старые файлы. Если вы экспортировали чаты в HTML раньше, переэкспортируйте их после обновления — или открывайте старые экспорты с отключённым JavaScript.
Что ответил Telegram
В нашу демонстрационную группу также вступил представитель службы безопасности Telegram под именем TS Sec. Он написал, что попробовал экспорт в другой группе, и спросил, получили ли мы что‑нибудь. Мы обсуждали демонстрацию прямо в чате — ещё до последующей переписки о публикации.

— 1 июня 2026 — находим баг.
— 3 июня — сообщаем о нём на security@telegram.org с PoC, видео и оценкой CVSS.
— 30 июня — написан фикс. Публичная дата коммита — 2 июля.
— 1 июля — Telegram подтверждает отчёт и предлагает вознаграждение $500. Мы отказываемся и просим отдать деньги на благотворительность. Там же просим согласовать дату публикации и предлагаем молчать до выхода фикса.
— 1 июля — Telegram отвечает, что не может одобрить публичное раскрытие.
— 3 / 14 июля — Beta v6.9.4 и стабильная версия v7.0.1 выходят с фиксом.
Вот главное из их ответа: «We also have considered the possibility of a public disclosure but we cannot approve it as disclosing even the already addressed issues could put more Telegram users at risk in the future. For instance, if information about a vulnerability is made public, malicious actors may attempt to exploit it thereby causing financial harm to Telegram users.»
Перевод: «Мы также рассматривали возможность публичного раскрытия, но не можем его одобрить: раскрытие даже уже исправленных уязвимостей может в будущем подвергнуть риску больше пользователей Telegram. Например, если информация об уязвимости станет публичной, злоумышленники могут попытаться её эксплуатировать, причинив финансовый ущерб пользователям Telegram».

Публичная политика баг‑баунти говорит о другом: если о проблеме рассказали публично до того, как её исправили, отчёт не получит вознаграждение. «Vulnerabilities that are disclosed to the public or to third parties before they are addressed are not eligible for our bug bounty program». Про то, что публикация после фикса требует одобрения Telegram, там нет ничего. NDA мы не подписывали.
Перечитайте их ответ: «even the already addressed issues» — «даже уже исправленных». Это не «подождите релиза». Фикс был написан за день до этого ответа. Это был отказ одобрить публикацию после фикса.
Почему мы всё равно публикуем
Три причины.
Люди. Старые файлы экспорта по‑прежнему опасны, а тихий патч никого не предупреждает — ни о переэкспорте чатов, ни о том, что старые файлы стоит открывать осторожнее. Именно из advisory пользователи узнают о проблеме.
Необновлённые клиенты. Фикс помогает только тем, кто обновляется, и он не переписывает файлы, которые уже лежат на диске.
Норма отрасли. Advisories и записи CVE дают защитникам понять, затронуты ли они, помогают специалистам по инцидентам знать, что искать, и делают возможной независимую проверку. Коммит с фиксом публичен; пояснения вокруг него — нет.
Для сравнения: Signal ведёт публичную страницу с описанием сообщённых проблем и именами исследователей. Chrome публикует release notes с номерами CVE и указанием исследователей. Apple добавляет номера CVE, когда может. Ни advisory от Telegram, ни публичной записи CVE по этой уязвимости до сих пор нет — при том, что коммит с фиксом лежит в открытом репозитории, и любой может его прочитать — но пояснений к нему нет.
Безопасность — это не отсутствие багов. Баги есть у всех. Это то, что вендор делает дальше: публикует advisory, присваивает CVE, даёт исследователям публиковать свои находки. Когда компания, построившая бренд на безопасности, говорит «нет» публичной документации уже исправленного бага, уместен вопрос: это защита пользователей или бренда?
Как мы тестировали
Мы сообщили о баге через официальную программу баг‑баунти Telegram, security@telegram.org. Всё тестировалось на наших собственных аккаунтах и в наших тестовых группах. Сетевой трафик PoC не выходил за пределы 127.0.0.1 — кроме стандартных вызовов Bot API, мы не трогали продакшн Telegram. От вознаграждения $500 мы отказались и попросили отдать его на благотворительность. Публикуем после выхода фикса и после того, как проверили его на релизных сборках. Рабочего эксплойта против живых систем в этой статье нет.
Скриншоты — из трёх отдельных контролируемых демонстраций, это не один непрерывный прогон. Все аккаунты и группы — наши. Баг исправлен.
Спасибо, что дочитали. Можете переслать статью коллеге.
Только обычной ссылкой. К пустым кнопкам у нас теперь вопросы.
Также хотим выразить благодарность Telegram за фикс. И отдельно — за заголовок этой статьи.
Денис Ростилов и Александр Ростилов, ExPatch Vulnerability Research

