Telegram Html Export Xss
A stored XSS in Telegram Desktop's HTML export pipeline lets a bot that never joins your group plant invisible JavaScript in an inline keyboard button. The payload sleeps in message history for months and detonates the moment a participant exports the chat and opens the HTML file — every message rendered in that document can be shipped to the attacker's server, and the page itself can be rewritten.
Analysis of Telegram Desktop (tdesktop) HTML export pipeline. Affected: exports produced by builds before v6.9.4 (Beta) / v7.0.1 (Stable); fixed in 8457d13a . Reported 2026-06-03, fix shipped 2026-07; as of publication no CVE has been assigned.
A stored XSS in Telegram Desktop lets an attacker plant invisible JavaScript in an exportable chat through a bot's inline keyboard button. The payload can sit in message history for months and detonates when a participant opens an HTML export page containing that message. No second click, no warning: every message and metadata field rendered in that document can be shipped to the attacker's server, and the page itself can be rewritten. The bot never joins the target chat — one forwarded message can be enough.
Prologue: A Monday Morning
9:47 a.m. Compliance at a fintech company has requested a full export of the engineering chat. Regulatory review — routine, happens twice a year. The lead developer opens Telegram Desktop, clicks the three dots in the corner, picks "Export chat history," format HTML, and opens the resulting file in Chrome.
The page loads. Messages appear. Timestamps, names, code snippets, internal API keys shared months ago, heated architecture debates, the thread where somebody pasted AWS credentials "just for a second."
What the developer doesn't notice: between the messages, inside an unremarkable "Open" button, a tag has already fired. In the 400 milliseconds the page took to render, every message in that document — months of engineering history — was packed into JSON and sent to a server in another country.
The developer sees a normal export. The attacker sees everything.
The message carrying the payload landed in the group seven months ago: a new colleague forwarded it, thinking it was a link to the corporate blog. The bot that produced the message was never in the group. It had no access to the group's ordinary traffic or prior history. It didn't need any.
One Missing Sanitization Call
The root cause is a single line in the Telegram Desktop source.
export_output_html.cpp , line 1752:
When exporting a chat to HTML, Telegram Desktop writes inline-button text straight into the HTML page — unescaped. SerializeString() , which escapes every HTML-dangerous character ( , & , " , ' ), converts newlines and Unicode line/paragraph separators to , and hex-encodes ASCII control characters, is defined in the same file and applied to message text, sender names, and other fields. It just wasn't applied to button text.
That means any HTML sitting in a button's text property renders as live markup in the exported page. tags included.
Why This Is Worse Than a Typical Stored XSS
Standard stored XSS requires the attacker to have write access to the target context. This one doesn't. Telegram's Bot API lets any bot create messages with inline keyboards. The text field accepts arbitrary Unicode, HTML tags included. And, critically: a URL-only inline keyboard satisfies CopyMarkupToForward and survives forwarding . The full predicate also permits games and some SwitchInline cases; the attack needs none of them.
That produces a chain with three properties that, taken together, turn "run-of-the-mill XSS" into a high-impact mass-exploitation vector:
No access required. The bot never joins the target chat. Someone else does the forwarding.
Invisible persistence. The payload sits in history for months or years. It only fires on export.
One-to-many. A single forward into a 200,000-member supergroup can compromise any participant whose export contains the message and who opens that HTML document.
Attack Scenario 1: Forwarding Through an Insider
Demonstration of the social-engineering vector: a recently added colleague forwards a bot's message into a work group. Another employee exports the chat to HTML and opens it — the page is replaced with a fake Telegram verification form (full DOM takeover via XSS). In parallel, every message rendered in the opened document is quietly exfiltrated to the attacker's server.
Video: Telegram Desktop stored XSS via HTML export — Scenario 1 (insider forwarding)
The attacker controls a bot — any bot, even one spun up five minutes ago via @BotFather. The bot sends a single message to an accomplice (or to any public chat the attacker is in):
The message looks completely normal in the demonstrated Telegram Desktop build. The button shows empty or near-empty text: Hangul Filler characters obscure the trailing tag, while the URL remains legitimate. No visual sign of an embedded script.
The accomplice — or anyone who sees it — forwards the message into the target group. Telegram preserves the inline button because it's URL-typed. The bot was never a member of the target group. It has zero API access to that group's messages.
Seven months later, someone runs an export.
Injection into Button Text
Injection into button text (stealthy): the tag is embedded in the button's text field, masked by invisible Unicode — the button looks empty or shows only the URL. Attribute values such as href were already escaped by pushTag() ; the confirmed injection point is the unescaped button text.
Attack Scenario 2: Direct Injection by the Bot
Demonstration: a utility bot — weather, polls, reminders — sends the payload directly into a group it has been added to. With privacy mode enabled, the bot does not receive ordinary group traffic or prior history. Its own message still becomes code when the poisoned export document opens, giving it access to every message rendered on that page.
Video: Telegram Desktop HTML Export XSS | Bot Privacy Mode PoC (Patched)
This scenario exposes the core paradox of the vulnerability. Telegram's bot privacy model exists to protect users:
Group admins see a clear indicator: "this bot has no access to messages."
Users trust this model. That is why groups freely add bots.
But HTML export destroys that boundary. It serializes the bot's own messages — inline keyboards included — into the exported page. The API withholds ordinary group traffic; the poisoned document hands the bot's script every message rendered on the page.
The bot escalates from no access to ordinary group traffic to browser-side access to every message rendered in the poisoned document — not through the Bot API, but through Telegram Desktop's export pipeline.
This is exactly the privilege boundary the Scope Changed metric ( S:C in CVSS) is meant to describe: the Bot API withholds ordinary group traffic, but the impact lands in the victim's browser context, where the injected code controls the opened export document.
When the exported HTML is opened, the injected script ( p.js ) runs immediately on page load — no additional click after the file is opened:
DOM Takeover: History Forgery and Phishing
Beyond quiet data theft, the XSS gives full control over the DOM. The PoC includes overlay.js , which replaces the entire export page with a convincing Telegram verification form:
The victim sees a "Verification required" prompt with Telegram branding
The real export contents are gone — completely replaced
In the demo, password fields are read-only placeholders and only a test email value is sent to 127.0.0.1 ; the same DOM control can be adapted for credential phishing
The victim has no reason to suspect a swap — they're looking at their own data in their own browser
The attacker can also silently modify the rendered history — change timestamps, rewrite sender names and message text, reorder, insert, or hide messages, and replace their surrounding context. In a legal setting or a compliance review, where a chat export functions as evidence, that is record tampering at the browser layer. The PoC does not alter Telegram's server-side history or write those changes back to the source HTML on disk.
Scaling: One Message, Mass Seeding
Demonstration of the collection panel used by the PoC. The final view shows three capture records from one chat and one IP: two records contain the same 14 rendered messages from four senders, while one contains no messages. The video proves collection from an opened poisoned export; it does not claim multiple victims, groups, contacts, or phone numbers.
Video: Telegram Desktop HTML Export XSS | Data Exfiltration PoC (Patched)
The Forwarding Amplifier
The attack scales through Telegram's own forwarding mechanism:
Every forwarded copy carries the payload independently. The bot joins none of these groups. If the forwarded message is included in an HTML export, opening that poisoned page triggers an independent exfiltration.
One important distinction in the attack surface. In groups and supergroups where content protection is disabled, any member can forward the message. That is the primary surface for mass seeding. Forwarding into channels requires posting rights, meaning the attacker needs either control of an admin account or a colluding insider. Channels remain in the threat model — compromised admin, or a targeted channel used against a company's employees — but they are not an automatic mass-distribution vector on their own.
The payload is persistent. It lives in chat history until the message is deleted. Members who join later can also receive it in their export when prior history is visible to them and the message falls within the exported range.
Every poisoned export page fires independently. A page that contains the payload can trigger when opened; another page without the payload does not.
Unlike a malicious attachment, the carrier is dormant text stored inside Telegram's own message database. The external script URL can be blocked, but the poisoned message itself is not a file to scan. It activates only when an unrelated, legitimate user action — exporting a chat — turns inert message data into executable HTML.
The gap between injection and detonation can be months or years . The attacker seeds the payload and waits.
The Vulnerable Code Path
Telegram Desktop's HTML export is handled in export_output_html.cpp . The function that writes inline buttons emits raw HTML:
Compare with how message text is written a few lines away:
SerializeString() escapes , & , " , ' into HTML entities, converts \n and Unicode line/paragraph separators to , and hex-encodes control characters — the standard set for neutralizing injected markup. Skipping this call for button text is the entire vulnerability. The fix is one function call.
How Long This Sat in Production
The vulnerable line block.append(button.text.toUtf8()) was introduced by commit 52c779bf ("Added support of inline markup to HTML export.", author 23rd ). It was authored on February 21, 2024 , committed on March 8, 2024 , and reached the stable v4.15.1 release that day. The fix was authored on June 30, 2026.
The vulnerability sat in production code for roughly two years and four months . Any HTML export created by a vulnerable build can still contain a live tag inside an inline button if the attacker seeded that payload into the exported range. The client-side patch does not rewrite files already on disk: those old exports remain dangerous when opened with JavaScript enabled.
Why the Telegram Client Wasn't Affected
Telegram's desktop and mobile clients render messages through their own UI framework, not through a browser engine. Button text is displayed as a flat string — HTML tags show up literally rather than being interpreted. That is why the injection was invisible in the client (the tag is just characters) but dangerous in the export (the browser interprets it as live code).
This created a false sense of safety: the button text "looked normal" in the app because the app doesn't parse HTML. But the export pipeline does — and it trusted the same text without sanitization.
Deliberate Conservative Scoring
We intentionally score this AV:L , not AV:N , and want to be open why.
Under a strict reading of FIRST 3.1 §2.1.1, Attack Vector is defined through the vulnerable component and its network reachability. The vulnerable component here is export_output_html.cpp — a local batch serializer that reads messages from the local cache only under explicit user action ("Export chat history") . It is not bound to the network stack. The network path (Bot API → Telegram servers → local tdesktop cache) traverses separate code that handles the data correctly; the vulnerability is not there. This shape is structurally closer to Follina (CVE-2022-30190, AV:L per NVD) than to SymStealer (CVE-2022-3656, AV:N per NVD), where the vulnerable Chrome renderer processes HTTP content directly at arrival.
Changing only AV:L to AV:N would produce 9.3, but AV:N is not the vector used in this report. The stored value reaches the client over Telegram's network; exploitation occurs when the local export serializer emits executable HTML and the user opens it. We keep the stricter 8.2 High score because a defensible vector matters more than a "Critical" label.
The practical impact remains severe: exfiltration of sensitive content from the opened document and full control over how that exported history is presented in the browser.
For Telegram (shipped): commit 8457d13a "Fix escaping in HTML export of keyboards." (John Preston; authored June 30, committed July 2, 2026) applies SerializeString() to button.text in export_output_html.cpp:1752 — the same sanitization already used for message text and other fields. The same commit closes a bonus vector : a JS-string injection into the onclick="return ShowTextCopied('…')" attribute of copy-callback buttons — content now escapes \\ and ' before being interpolated into the JS literal.
Final diff ( export_output_html.cpp ):
The patch first reached a tagged release in Telegram Desktop Beta v6.9.4 , published on GitHub on July 3, 2026. For stable-channel users, the first GitHub release containing the fix is v7.0.1 , published on July 14, 2026.
Beta channel: update to v6.9.4 or later.
Stable channel: update to v7.0.1 or a newer release containing the fix.
If you exported chats to HTML before the fix: the files on your disk may contain dormant payloads. Re-export the chats after updating, or open old exports with JavaScript disabled.
Be cautious opening any HTML chat export produced before the patch date, especially from large groups where the origin of individual messages is hard to verify.
On Disclosure: What Telegram Says in Public and What It Writes in Private
This section is not hurt feelings and not money. It is how one of the world's largest messengers, with security at the center of its marketing, handles vulnerability information — and why that matters to every user.
What the Public Policy Says
The Telegram Bug Bounty Program states the following consequence for pre-fix disclosure:
"Vulnerabilities that are disclosed to the public or to third parties before they are addressed are not eligible for our bug bounty program."
"Vulnerabilities that are disclosed to the public or to third parties before they are addressed are not eligible for our bug bounty program."
The published rule makes public or third-party disclosure before a fix ineligible for a bounty. The page does not say that post-fix publication requires Telegram's approval. No separate NDA or confidentiality agreement was executed for this report.
What Happened in Practice
I sent a report with a full PoC and video demonstration. Telegram confirmed the vulnerability and offered a bounty. I declined the bounty and asked that the amount be redirected to charity. I requested a coordinated publication date and explicitly offered to remain silent until the patch shipped.
"Could you also let me know the expected timeline for the fix, and whether there's a coordinated disclosure date you'd prefer? I'm happy to hold off on any public disclosure until the patch has shipped."
"Could you also let me know the expected timeline for the fix, and whether there's a coordinated disclosure date you'd prefer? I'm happy to hold off on any public disclosure until the patch has shipped."
And here is Telegram's answer:
"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 a vulnerability is made public, malicious actors may attempt to exploit it thereby causing financial harm to Telegram users."
"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 a vulnerability is made public, malicious actors may attempt to exploit it thereby causing financial harm to Telegram users."
Re-read that: "even the already addressed issues." This was not a request to wait for the patch. Telegram explicitly wrote that it could not approve public disclosure even after an issue had been addressed. That is a refusal to approve post-fix disclosure.
Let's take the argument apart on the merits.
"Disclosing even already-addressed issues could put users at risk" is an argument against the standard post-fix advisory model used across the security industry. Advisories and CVE records give defenders a reason to update, let incident responders assess exposure, and make independent review possible. A silent patch gives them none of that context.
"Malicious actors may attempt to exploit it" — after the fix, the remaining targets are unpatched clients and old HTML exports that the client update cannot rewrite. That is exactly why users need an advisory: to update the client and treat old exports as untrusted active content. I offered publication only after the patch shipped. Telegram refused to approve even that.
What this means in practice: Telegram chose a silent patch for a high-impact vulnerability and refused to approve post-fix publication. The fix commit is public, but as of September 11, 2026, Telegram has published no security advisory and there is no public CVE or NVD record for this issue. Users therefore receive no vendor warning that a pre-fix HTML export may execute attacker-controlled code when opened.
And one more detail from the tdesktop git metadata. The fix commit ( 8457d13a ) was authored on June 30, 2026 — a day before Telegram confirmed the report and offered a bounty. Its public committer timestamp is July 2. That does not prove the commit was already public on July 1; it does prove the fix had been prepared before the refusal. More importantly, Telegram's wording was not limited to the release window: it explicitly covered already addressed issues . This was a position against post-fix disclosure, not a request for a few more days to ship.
The Question Every User Should Ask
Telegram builds its positioning around security as one of its core pillars. telegram.org emphasizes encryption, self-destruct, and the promise that Telegram "keeps your messages safe from hacker attacks." Security is a load-bearing part of the product's marketing narrative.
But security is not the absence of vulnerabilities (no one is free of those). Security is how you handle them . Whether you publish an advisory. Whether you assign CVEs. Whether you let researchers publish. Whether you allow independent audit.
Signal maintains a public Security Acknowledgments page naming reported issues and researchers.
Google Chrome publishes security release notes with CVE identifiers and researcher credit.
Apple publishes security release notes and adds CVE identifiers when possible.
Telegram asked this researcher to stay silent even after the fix. The code change is visible, but there is no Telegram advisory and no public CVE record for this issue as of September 11, 2026.
Ask yourself: when a vendor that built its brand on security actively obstructs public documentation of its own vulnerabilities — is it protecting your data, or protecting its reputation?
I offered Telegram full silence until the patch and publication only afterward. They refused to approve it. Their published program says that disclosure before an issue is addressed makes a report ineligible for a bounty; it does not state that post-fix publication requires approval. I declined the bounty voluntarily, and the fix has shipped.
This is post-fix disclosure after advance vendor notification. Telegram did not agree to a coordinated publication date; it rejected post-fix disclosure altogether.
Responsible Disclosure
The vulnerability was reported through Telegram's official bug bounty program at [email protected] . All testing was performed on the researcher's own accounts and test groups. The PoC code uses only 127.0.0.1 — no interaction with Telegram production infrastructure beyond standard Bot API calls.
The $500 bounty was voluntarily declined with a request to redirect it to charity. A coordinated publication date was requested; Telegram refused to approve disclosure even after the issue had been addressed. This writeup is prepared after the fix shipped and was verified in released builds. A working exploit targeting live systems is not included.
Denis Rostilov — Security Researcher Aleksander Rostilov — Security Researcher [email protected] · expatch.com
No public CVE record as of 2026-09-11 | Fix: tdesktop 8457d13a (Beta v6.9.4 / Stable v7.0.1) | Telegram Bug Bounty Program
Хранимый XSS в пайплайне HTML-экспорта Telegram Desktop позволяет боту, который никогда не вступал в вашу группу, внедрить невидимый JavaScript в inline-кнопку. Пейлоад месяцами спит в истории сообщений и детонирует в момент, когда участник экспортирует чат и открывает HTML-файл: каждое отрисованное в документе сообщение может уйти на сервер атакующего, а сама страница — быть полностью переписана.
Анализ пайплайна HTML-экспорта Telegram Desktop (tdesktop). Уязвимы: экспорты, созданные сборками до v6.9.4 (Beta) / v7.0.1 (Stable); исправлено в 8457d13a . Репорт отправлен 2026-06-03, фикс выпущен в июле 2026; на момент публикации CVE не присвоен.
Stored XSS в Telegram Desktop позволяет внедрить невидимый JavaScript в доступный для экспорта чат через inline-кнопку бота. Пейлоад может месяцами лежать в истории и срабатывает, когда участник открывает содержащую его страницу HTML-экспорта. После открытия второй клик не нужен: сообщения и метаданные, отрисованные в этом документе, могут уйти на сервер атакующего, а сама страница — быть полностью переписана. Бот не вступает в целевой чат: достаточно одного пересланного сообщения.
Пролог: утро понедельника
9:47 утра. Отдел комплаенса финтех-компании запросил полный экспорт рабочего чата инженеров. Регуляторная проверка — рутина, бывает дважды в год. Ведущий разработчик открывает Telegram Desktop, нажимает три точки в углу, выбирает «Экспорт истории чата», формат HTML, и открывает получившийся файл в Chrome.
Страница загружается. Появляются сообщения. Таймстемпы, имена, фрагменты кода, внутренние API-ключи, которыми делились месяцы назад, жаркие дискуссии об архитектуре, тред, где кто-то вставил AWS-креды «буквально на секунду».
Чего разработчик не замечает: между сообщениями, внутри ничем не примечательной кнопки «Open», тег уже отработал. За 400 миллисекунд, пока страница рендерилась, каждое сообщение в этом документе — месяцы инженерной истории — упаковано в JSON и отправлено на сервер в другой стране.
Разработчик видит обычный экспорт. Атакующий видит всё.
Сообщение с пейлоадом попало в группу семь месяцев назад: новый коллега переслал его, приняв за ссылку на корпоративный блог. Бот, создавший сообщение, никогда не состоял в группе. У него не было доступа к обычному трафику группы или её прошлой истории. Ему и не нужно было.
Один пропущенный вызов функции
Корень проблемы — одна строка в исходном коде Telegram Desktop.
export_output_html.cpp , строка 1752:
При экспорте чата в HTML Telegram Desktop записывает текст inline-кнопок напрямую в HTML-страницу — без экранирования. Функция SerializeString() , которая экранирует все опасные для HTML символы ( , & , " , ' ), конвертирует переводы строк и Unicode-разделители в и hex-кодирует ASCII control-символы, определена в том же файле и применяется к тексту сообщений, именам отправителей и другим полям. Но к тексту кнопок она не применялась.
Это значит, что любой HTML в свойстве text кнопки рендерится как живая разметка в экспортированной странице. Включая теги .
Почему это хуже обычного XSS
Типичный stored XSS требует, чтобы атакующий имел доступ на запись в целевом контексте. Этот — нет. Bot API Telegram позволяет любому боту создавать сообщения с inline-кнопками. Поле text принимает произвольный Unicode, включая HTML-теги. И критически важно: inline-клавиатура только с URL-кнопкой проходит CopyMarkupToForward и сохраняется при пересылке . Полный предикат также допускает игры и некоторые варианты SwitchInline , но для этой атаки они не нужны.
Это создаёт цепочку с тремя свойствами, которые в сумме превращают «обычный XSS» в high-impact вектор массовой эксплуатации:
Доступ не нужен. Бот не вступает в целевой чат. Сообщение пересылает третье лицо.
Невидимая персистентность. Пейлоад лежит в истории месяцами или годами. Срабатывает только при экспорте.
Один ко многим. Одно пересланное сообщение в супергруппу на 200 000 участников может скомпрометировать любого, чей экспорт содержит это сообщение и кто откроет такую HTML-страницу.
Сценарий атаки 1: пересылка через инсайдера
Демонстрация социально-инженерного вектора: недавно добавленный коллега пересылает сообщение бота в рабочую группу. Другой сотрудник экспортирует чат в HTML и открывает его — страница подменяется фейковой формой верификации Telegram (полный захват DOM через XSS). Параллельно каждое сообщение, отрисованное в открытом документе, тихо утекает на сервер атакующего.
Видео: Telegram Desktop stored XSS via HTML export — Scenario 1 (insider forwarding)
Атакующий управляет ботом — любым ботом, пусть даже созданным пять минут назад через @BotFather. Бот отправляет одно сообщение сообщнику (или в любой публичный чат, где атакующий состоит):
В продемонстрированной сборке Telegram Desktop сообщение выглядит совершенно нормально. Символы Hangul Filler скрывают хвостовой тег, а URL остаётся легитимным. В интерфейсе нет видимого признака встроенного скрипта.
Сообщник (или любой, кто его увидит) пересылает сообщение в целевую группу. Telegram сохраняет inline-кнопку, потому что она URL-типа. Бот никогда не был участником целевой группы. У него ноль доступа к сообщениям группы через API.
Семь месяцев спустя кто-то делает экспорт чата.
Инъекция в текст кнопки
Инъекция в текст кнопки (скрытная): тег встроен в поле text кнопки и замаскирован невидимым Unicode — кнопка выглядит пустой или показывает только URL. Значения HTML-атрибутов, включая href , уже экранировались в pushTag() ; подтверждённая точка инъекции — неэкранированный текст кнопки.
Сценарий атаки 2: прямая инъекция ботом
Демонстрация: вспомогательный бот — погодный, опросный, напоминалка — отправляет пейлоад напрямую в группу, в которую его добавили. При включённом privacy mode бот не получает обычный трафик группы и прошлую историю. Но его собственное сообщение превращается в код при открытии отравленного экспорта и получает доступ ко всем сообщениям, отрисованным на этой странице.
Видео: Telegram Desktop HTML Export XSS | Bot Privacy Mode PoC (Patched)
Этот сценарий вскрывает ключевой парадокс уязвимости. Модель приватности ботов в Telegram создана для защиты пользователей:
По умолчанию боты с включённым privacy mode не получают обычный трафик группы и прошлую историю. Они по-прежнему получают предусмотренные Telegram команды, ответы, сообщения, отправленные через бота, и служебные сообщения.
Администраторы группы видят чёткий индикатор: «бот не имеет доступа к сообщениям».
Пользователи доверяют этой модели. Поэтому группы свободно добавляют ботов.
Но HTML-экспорт уничтожает эту границу. Он сериализует собственные сообщения бота — включая inline-кнопки — в экспортированную страницу. Bot API не отдаёт боту обычный трафик группы; отравленный документ отдаёт его скрипту каждое сообщение, отрисованное на странице.
Бот эскалирует от отсутствия доступа к обычному трафику группы до доступа из браузера ко всем сообщениям, отрисованным в отравленном документе, — не через Bot API, а через экспортный пайплайн Telegram Desktop.
Это именно та граница привилегий, которую описывает метрика Scope Changed ( S:C в CVSS): Bot API не отдаёт обычный трафик группы, но импакт происходит в контексте браузера жертвы, где инъецированный код контролирует открытый документ экспорта.
При открытии экспортированного HTML инъецированный скрипт ( p.js ) исполняется сразу при загрузке страницы — после открытия файла дополнительный клик не нужен:
Основная эксфильтрация
Захват DOM: подделка истории и фишинг
Помимо тихой кражи данных, XSS даёт полный контроль над DOM. PoC включает overlay.js , который заменяет всю страницу экспорта убедительной формой верификации Telegram:
Жертва видит «Необходима верификация» с брендингом Telegram
Реальное содержимое экспорта исчезло — полностью заменено
В демо поля пароля — read-only placeholders, а на 127.0.0.1 отправляется только тестовый email; тот же контроль DOM можно адаптировать под credential phishing
У жертвы нет причин подозревать подмену — она просматривает собственные данные в собственном браузере
Атакующий также может тихо модифицировать отображаемую историю — менять даты, имена отправителей и текст сообщений, переставлять, вставлять или скрывать сообщения и подменять их контекст. В юридическом контексте или при комплаенс-проверке, где экспорт чата выступает доказательством, это фальсификация записей на уровне браузера. PoC не изменяет серверную историю Telegram и не записывает изменения обратно в исходный HTML-файл на диске.
Масштабирование: одно сообщение — массовое заражение экспортов
Демонстрация панели сбора, использованной в PoC. В финальном кадре — три записи из одного чата и с одного IP: две содержат один и тот же набор из 14 отрисованных сообщений от четырёх отправителей, одна не содержит сообщений. Видео доказывает сбор данных из открытого отравленного экспорта; оно не заявляет несколько жертв, групп, контакты или телефонные номера.
Видео: Telegram Desktop HTML Export XSS | Data Exfiltration PoC (Patched)
Атака масштабируется через собственный механизм пересылки Telegram:
Каждая пересланная копия несёт пейлоад независимо. Бот не вступает ни в одну из этих групп. Если пересланное сообщение попадает в HTML-экспорт, открытие отравленной страницы запускает независимую эксфильтрацию.
Важное различие в поверхности атаки. В группах и супергруппах , где отключена защита контента, сообщение может переслать любой участник. Это основная поверхность массового рассеивания. Публикация в каналах требует соответствующих прав: атакующему нужен контроль над admin-аккаунтом или insider. Каналы остаются в модели угроз — например, при компрометации администратора или атаке через корпоративный канал, — но сами по себе не дают автоматического массового распространения.
Пейлоад персистентен. Он живёт в истории чата до удаления сообщения. Участники, вступившие позже, также могут получить его в экспорт, если им видна предыдущая история и сообщение входит в экспортируемый диапазон.
Каждая отравленная страница экспорта срабатывает независимо. Страница с пейлоадом может запустить его при открытии; другая страница без пейлоада — нет.
Фактор бомбы замедленного действия
В отличие от вредоносного вложения, носитель здесь — спящий текст в собственной базе сообщений Telegram. Внешний URL скрипта можно заблокировать, но само отравленное сообщение не является файлом, который можно просканировать. Оно активируется только тогда, когда не связанное с ним легитимное действие пользователя — экспорт чата — превращает инертные данные сообщения в исполняемый HTML.
Разрыв между инъекцией и детонацией может составлять месяцы или годы . Атакующий засеивает пейлоад и ждёт.
HTML-экспорт Telegram Desktop обрабатывается в файле export_output_html.cpp . Функция записи inline-кнопок формирует сырой HTML:
Сравните с тем, как записывается текст сообщения на соседних строках:
SerializeString() экранирует , & , " , ' в HTML-сущности, конвертирует \n и Unicode line/paragraph separators в , hex-кодирует контрольные символы — стандартный набор для нейтрализации инъецированной разметки. Пропуск этой функции для текста кнопок — это вся уязвимость целиком. Фикс — один вызов функции.
Сколько времени это жило в prod
Уязвимая строка block.append(button.text.toUtf8()) введена коммитом 52c779bf («Added support of inline markup to HTML export.», автор 23rd ). Коммит был заавторен 21 февраля 2024 года , закоммичен 8 марта 2024 года и в тот же день вошёл в stable v4.15.1. Фикс был заавторен 30 июня 2026 года.
Уязвимость жила в prod-коде примерно 2 года и 4 месяца . Любой HTML-экспорт, созданный уязвимой сборкой, всё ещё может содержать активный внутри inline-кнопки, если атакующий засеял пейлоад в вошедший в экспорт диапазон. Патч клиента не переписывает уже созданные файлы: такие старые экспорты остаются опасными при открытии с включённым JavaScript.
Почему клиент Telegram не был затронут
Десктопный и мобильный клиенты Telegram рендерят сообщения через собственный UI-фреймворк, а не через браузерный движок. Текст кнопки отображается как плоская строка — HTML-теги показываются буквально, а не интерпретируются. Поэтому инъекция была невидима в клиенте (тег — просто символы), но опасна в экспорте (браузер интерпретирует его как живой код).
Это создавало ложное чувство безопасности: текст кнопки «выглядел нормально» в приложении, потому что приложение не парсит HTML. Но пайплайн экспорта парсит — и он доверился тому же тексту без санитизации.
Осознанно консервативная оценка
Мы намеренно ставим AV:L , не AV:N , и обосновываем это открыто.
По строгому чтению FIRST 3.1 §2.1.1 вектор атаки определяется через уязвимый компонент и его сетевую доступность. Уязвимый компонент здесь — export_output_html.cpp , локальный batch-сериализатор, читающий сообщения из локального кэша по явной команде пользователя (кнопка «Export chat history») . Он не bound to network stack. Сетевой путь (Bot API → сервера Telegram → локальный кэш tdesktop) проходит через отдельный, корректно обрабатывающий данные код — уязвимости в нём нет. Это структурно ближе к Follina (CVE-2022-30190, AV:L per NVD), чем к SymStealer (CVE-2022-3656, AV:N per NVD), где уязвимый рендерер Chrome обрабатывает HTTP-контент напрямую в момент прихода.
Если изменить только AV:L на AV:N , получится 9.3, но в этом отчёте используется не AV:N . Сохранённое значение приходит в клиент через сеть Telegram; эксплуатация происходит, когда локальный сериализатор экспорта генерирует исполняемый HTML, а пользователь открывает его. Мы оставляем более строгую оценку 8.2 High , потому что защищаемый вектор важнее ярлыка «Critical».
Практический импакт остаётся тяжёлым: эксфильтрация чувствительного содержимого открытого документа и полный контроль над тем, как экспортированная история выглядит в браузере.
Для Telegram (реализовано): в коммите 8457d13a «Fix escaping in HTML export of keyboards.» (John Preston; заавторен 30 июня, закоммичен 2 июля 2026 года) к button.text в export_output_html.cpp:1752 применён SerializeString() — та же санитизация, которая уже использовалась для текста сообщений и других полей. В том же коммите закрыт сопутствующий вектор : JS-string-инъекция в атрибут onclick="return ShowTextCopied('…')" для copy-callback кнопок — теперь content экранирует \\ и ' перед подстановкой в JS-литерал.
Итоговый диф ( export_output_html.cpp ):
Патч впервые вошёл в тегированный релиз Telegram Desktop Beta v6.9.4 , опубликованный на GitHub 3 июля 2026 года. Для stable-канала первым GitHub-релизом с исправлением стала v7.0.1 , опубликованная 14 июля 2026 года.
Beta-канал: обновитесь до v6.9.4 или новее.
Stable-канал: обновитесь до v7.0.1 или более новой версии, содержащей исправление.
Если вы экспортировали чаты в HTML до фикса: файлы на вашем диске могут содержать спящие пейлоады. Переэкспортируйте чаты после обновления или открывайте старые экспорты с отключённым JavaScript.
Будьте осторожны при открытии любого HTML-экспорта чата, созданного до даты патча, особенно из больших групп, где происхождение сообщений сложно проверить.
О раскрытии: что говорит Telegram публично и что пишет приватно
Этот раздел не про обиду и не про деньги. Он о том, как один из крупнейших мировых мессенджеров, поставивший безопасность в центр своего маркетинга, обращается с информацией об уязвимостях — и почему это касается каждого пользователя.
Что написано в публичной политике
На странице Telegram Bug Bounty Program для раскрытия до исправления указано следующее последствие:
"Vulnerabilities that are disclosed to the public or to third parties before they are addressed are not eligible for our bug bounty program."
"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 или соглашения о конфиденциальности по этому отчёту не заключалось.
Что произошло на практике
Я отправил отчёт с полным PoC и видеодемонстрацией. Telegram подтвердил уязвимость и предложил вознаграждение. Я отклонил его и попросил направить сумму на благотворительность. Я запросил согласованную дату публикации и прямо предложил молчать до выпуска патча.
"Could you also let me know the expected timeline for the fix, and whether there's a coordinated disclosure date you'd prefer? I'm happy to hold off on any public disclosure until the patch has shipped."
"Could you also let me know the expected timeline for the fix, and whether there's a coordinated disclosure date you'd prefer? I'm happy to hold off on any public disclosure until the patch has shipped."
И вот ответ Telegram:
"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 a vulnerability is made public, malicious actors may attempt to exploit it thereby causing financial harm to Telegram users."
"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 a vulnerability is made public, malicious actors may attempt to exploit it thereby causing financial harm to Telegram users."
Перечитайте: «даже уже исправленные уязвимости» . Это не просьба подождать до патча. Telegram прямо написал, что не может одобрить публичное раскрытие даже после устранения проблемы. Это отказ одобрить публикацию после фикса.
Давайте разберём их аргумент по существу.
«Раскрытие даже исправленных уязвимостей может подвергнуть пользователей риску» — это аргумент против стандартной модели post-fix advisory, принятой в индустрии ИБ. Advisory и записи CVE дают защитникам причину обновиться, позволяют incident response оценить экспозицию и делают независимую проверку возможной. Тихий патч не даёт им этого контекста.
«Злоумышленники могут попытаться эксплуатировать её» — после фикса целями остаются необновлённые клиенты и старые HTML-экспорты, которые обновление клиента переписать не может. Именно поэтому пользователям нужен advisory: чтобы обновить клиент и считать старые экспорты недоверенным активным контентом. Я предложил публикацию только после выхода патча. Telegram отказался одобрить даже это.
Что это значит на практике: Telegram выбрал тихий патч для high-impact уязвимости и отказался одобрить публикацию после исправления. Коммит с фиксом публичен, но на 11 сентября 2026 года Telegram не выпустил security advisory, а для этой уязвимости нет публичной записи CVE или NVD. Пользователь не получает предупреждения вендора о том, что созданный до фикса HTML-экспорт может исполнять код атакующего при открытии.
И ещё одна деталь из метаданных tdesktop. Коммит с фиксом ( 8457d13a ) был заавторен 30 июня 2026 года — за сутки до того, как Telegram подтвердил отчёт и предложил вознаграждение. Его публичный committer timestamp — 2 июля. Это не доказывает, что 1 июля коммит уже был публичен; это доказывает, что исправление было подготовлено до отказа. Важнее другое: формулировка Telegram не ограничивалась окном до релиза — она прямо охватывала уже исправленные уязвимости . Это позиция против post-fix disclosure, а не просьба дать ещё несколько дней на выпуск патча.
Вопрос, который должен задать каждый пользователь
Telegram выстраивает своё позиционирование вокруг безопасности как одного из ключевых столбов. Главная страница telegram.org подчёркивает шифрование, self-destruct и обещание «keeps your messages safe from hacker attacks». Безопасность — это несущая часть маркетингового нарратива продукта.
Но безопасность — это не отсутствие уязвимостей (их нет ни у кого). Безопасность — это как вы с ними обращаетесь . Публикуете ли вы advisory. Присваиваете ли CVE. Даёте ли исследователям публиковать. Позволяете ли независимый аудит.
Signal ведёт публичную страницу Security Acknowledgments с описанием проблем и именами исследователей.
Google Chrome публикует security release notes с номерами CVE и кредитами исследователей.
Apple публикует security release notes и добавляет номера CVE, когда это возможно.
Telegram попросил этого исследователя молчать даже после фикса. Изменение кода видно, но на 11 сентября 2026 года нет advisory от Telegram и публичной записи CVE для этой уязвимости.
Спросите себя: когда вендор, построивший бренд на безопасности, активно препятствует публичной документации своих уязвимостей — он защищает ваши данные или свою репутацию?
Я предложил Telegram полное молчание до патча и публикацию только после. Они отказались её одобрить. В опубликованной программе сказано, что раскрытие до устранения проблемы лишает отчёт права на вознаграждение; там не сказано, что публикация после исправления требует одобрения. Я добровольно отказался от вознаграждения, а исправление уже выпущено.
Это post-fix disclosure после заблаговременного уведомления вендора. Telegram не согласовал дату координированной публикации — он отверг публикацию после фикса как таковую.
Ответственное раскрытие
Уязвимость была сообщена через официальную bug bounty программу Telegram на [email protected] . Всё тестирование проводилось на собственных аккаунтах и тестовых группах исследователя. Код PoC использует исключительно 127.0.0.1 — к продакшен-инфраструктуре Telegram не обращались, кроме стандартных вызовов Bot API.
Вознаграждение в размере $500 было добровольно отклонено с просьбой направить его на благотворительность. Была запрошена согласованная дата публикации; Telegram отказался одобрить раскрытие даже после устранения проблемы. Этот райтап подготовлен после выпуска исправления и его проверки в релизных сборках. Работающий эксплойт, нацеленный на живые системы, не включён.
Denis Rostilov — Security Researcher Aleksander Rostilov — Security Researcher [email protected] · expatch.com
Публичной записи CVE нет на 2026-09-11 | Fix: tdesktop 8457d13a (Beta v6.9.4 / Stable v7.0.1) | Telegram Bug Bounty Program
The full story
This article is one source in a clustered incident — the cluster page carries the summary, timeline and every other outlet covering it.
