Что случилось с HTML-виджетами Telegram: короткий ответ и хронология
Через день после публикации туториала по HTML-виджетам Telegram исследователь V12 показала, как через виджет запустить калькулятор на Android. Демонстрация подтвердила главное: ошибки в движке QuickJS-ng позволяют выйти за пределы логики виджета и выполнить на устройстве пользователя код, которого там быть не должно.
Павел Дуров ответил постом, где заявил, что проблемы не существует, а сценарий уже полностью закрыт на стороне сервера. Исправления при этом дошли до Telegram Desktop только в версии 7.4.3, а на Android в сборке 13.2.2 движок виджетов вынесли в отдельный изолированный процесс без доступа к файлам и сети.
Хронология первой недели виджетов по разбору на Habr выглядит так:
| Дата | Событие |
|---|---|
| 5 ноября | Telegram Desktop 7.4.0, в описании релиза одна строка: «HTML widgets in bots» |
| 6 ноября | Движок виджетов v0.6.1: семь исправлений, одно из них Telegram называет «the reported RCE» |
| 9 ноября | Виджеты открыли всем ботам в Bot API 10.4. В тот же день вышел аудит QuickJS-ng на 27 находок |
| 10 ноября | Движок v0.6.4: ещё пять ошибок памяти и вторая ошибка с выполнением кода, найденная на Android 13.2.1 |
| 11 ноября | Ролик V12 с калькулятором на Android. Telegram Desktop 7.4.2 |
| 13 ноября | Ночь по Москве: Telegram Desktop 7.4.3. Днём появляются сообщения, что виджеты перестали работать |
Ключевая деталь: первая запись о RCE в журнале исправлений появилась 6 ноября, за три дня до того, как виджеты открыли всем ботам. То есть правки шли параллельно с раскаткой функции, а не после публичного скандала. Хронология и детали восстановлены по материалу «Уязвимости нет, и мы её исправили»: первая неделя HTML-виджетов в Telegram.
Почему заявление Дурова и закрытый сценарий вызвали вопросы
Формулировка из поста звучит так: «Проблемы не существует. ... Даже этот сценарий уже полностью закрыт на стороне сервера». Две части этого утверждения плохо стыкуются между собой. Если проблемы нет, закрывать на сервере нечего. Если сценарий закрывали, значит, воспроизведение как минимум существовало.
Дуров уточнил, что для атаки пользователь должен был сначала запустить стороннего бота. Это сужает круг потенциальных жертв: нужен запуск незнакомого бота и открытие его виджета. Риск от этого не исчезает, потому что запуск бота выглядит обычным действием, а сам виджет в интерфейсе не отличим от безопасного.
Как именно сценарий закрыли на сервере, публично не объяснили. 13 ноября появились сообщения, что виджеты перестали работать. Совпадают ли эти два события, можно только предполагать, и предполагать в обе стороны одинаково легко. Твердого технического описания серверной фильтрации нет.
Что такое HTML-виджеты и почему они расширили поверхность атаки
HTML-виджеты дают ботам возможность встраивать интерактивные мини-приложения прямо в сообщение. Анонс Дурова звучал как «Any Telegram message can now become an app»: в чате появляется кнопка, по нажатию открывается интерфейс, который рисует клиент, а логику выполняет JavaScript.
Исполняет этот JavaScript движок QuickJS-ng. Он компактный, встраивается в клиент и работает на всех платформах, от десктопа до Android. Компактность и есть источник риска: чем меньше кода, тем выше цена ошибки в управлении памятью.
Поверхность атаки выросла по простой причине. Код виджета приходит от бота, то есть от третьей стороны, и исполняется на устройстве пользователя рядом с его чатами, файлами и сессией. Ошибка работы с памятью в движке превращается в потенциальный RCE: чужой код выходит за рамки логики виджета. Клиент, который рисует чужой интерфейс, обязан исходить из того, что этот интерфейс враждебный.
Технический разбор уязвимостей QuickJS-ng: use-after-free и use-after-return
Обе публично описанные ошибки относятся к времени жизни объектов в памяти. В первом случае объект освободили, а ссылку на него сохранили. Во втором объект пережил область, в которой ему было позволено существовать. Дальше любой доступ к такой памяти дает либо чтение мусора, либо запись туда, где уже лежат другие данные.
Use-after-free при перекомпиляции регулярки во время replace
Сценарий с replace выглядит безобидно. Виджет вызывает замену по регулярному выражению, колбэк удаляет совпадения, и движок посреди этой операции заново компилирует ту же самую регулярку. Старый байткод к этому моменту уже освобожден, но ссылка на него остается живой. Следующее обращение идет в освобожденную память.
Опасность здесь в триггере. Это не экзотический ввод и не редкая конструкция языка, а обычный JavaScript внутри виджета. Достаточно сочетания замены с удалением совпадений и повторного использования того же выражения. Ошибки такого класса плохо ловятся обычными тестами: код проходит по happy path, а падение случается на конкретном порядке вызовов.
Use-after-return из-за await using внутри обычной функции
Вторая ошибка связана с await using внутри обычной функции. Время жизни объекта не совпадает с кадром стека: после возврата из функции остается обращение к области, которая уже невалидна. Это ошибка времени жизни, а не забытый free, поэтому обычная проверка «все ли освободили» её не отлавливает.
Последствия одинаковы для обоих классов. Чтение или запись освобожденной памяти ломают инварианты движка, а при удачном раскладе дают атакующему контроль над выполняемым кодом. Демонстрация V12 с калькулятором на Android - иллюстрация именно этого исхода, пусть и в безобидной форме. Подробности механики обоих багов разобраны в том же материале о первой неделе виджетов: habr.com/ru/articles/1107658.
Номеров CVE, готовых эксплойтов и точных патчей в публичном описании нет, так что оценивать приходится по классу ошибки и по тому, как быстро вышли сборки.
Как нейросети искали баги в QuickJS-ng: Claude Fable 5.1 и Mythos 5.2
Часть багов разработчики нашли с помощью нейросетей. Аудит QuickJS-ng с Claude Fable 5.1 дал 27 находок, ещё одну ошибку памяти выявила Mythos 5.2. Это уже не эксперимент на синтетическом примере, а рабочий проход по коду реального движка, который стоит в клиенте с миллионами установок.
Что именно дает такой аудит: модель быстро читает большие объемы кода, выделяет места с ручным управлением памятью, предлагает сценарии, где порядок вызовов ломает инварианты, и генерирует заготовки тестов. Человек с той же задачей идет последовательно и упирается в объем.
Чего аудит не дает: гарантии. В источнике нет данных, сколько из 27 находок подтвердились как реальные уязвимости, а сколько оказались ложными срабатываниями или спорными местами. Число находок и число исправленных багов - разные величины, и подменять одно другим нельзя.
Полезный ориентир для сравнения подходов дает эксперимент Project Zero: фаззер Fuzzilli против LLM-агента BigSleep на движке V8 дал 45 и 37 исправленных уязвимостей за полгода при пересечении около четверти. Вывод оттуда переносится и на QuickJS-ng: автоматический поиск и нейросетевой аудит пока дополняют друг друга, а не заменяют.
Где AI-аудит помогает, а где нужна ручная проверка
Нейросеть хорошо масштабирует первичный обзор и поиск паттернов опасной работы с памятью. Слабые места предсказуемы: ложные срабатывания, неполный контекст при больших файлах, уверенный тон в неверных выводах. Находку нужно воспроизвести, иначе она остается гипотезой.
Рабочий порядок проверки выглядит так:
- зафиксировать версию движка и коммит, на котором проводился аудит, иначе находки нечем воспроизвести;
- для каждой находки собрать минимальный сценарий и прогнать его под санитайзерами памяти;
- отделять подтвержденные уязвимости от гипотез в самом отчете, а не в переписке;
- проверять не только сам баг, но и путь, которым до него доходит код из недоверенного источника;
- повторять аудит после крупных правок в управлении памятью, потому что старые выводы устаревают.
Для проекта, который встраивает чужой JavaScript в клиент, такой цикл дешевле, чем разбор последствий после публичной демонстрации.
Исправления в Telegram Desktop 7.4.3 и изоляция движка на Android 13.2.2
До десктопа исправления дошли не сразу. 5 ноября вышел Telegram Desktop 7.4.0 с виджетами, 11 ноября появился 7.4.2, и только 13 ноября, ночью по Москве, выпустили 7.4.3. Между 7.4.0 и 7.4.3 пользователи работали на сборках, где часть ошибок памяти ещё не была закрыта.
На Android изменения глубже. В сборке 13.2.2 движок виджетов вынесли в отдельный изолированный процесс без доступа к файлам и сети, а число графических эффектов ограничили. Разница принципиальная: 7.4.3 закрывает конкретные баги, 13.2.2 меняет архитектуру так, чтобы следующая ошибка в движке упиралась в песочницу.
Изоляция процесса ограничивает ущерб. Даже при успешной эксплуатации кода в движке атакующий остается в процессе без файлов и сети, и ему нужен второй шаг для выхода наружу. Лимит графических эффектов добавляет стабильности и убирает часть кода, который обрабатывает параметры от бота.
Полной гарантии это не дает. Песочницы обходят через ошибки в самом механизме изоляции и через каналы связи между процессами, а детали реализации на Android не опубликованы. Утверждать, что RCE на Android закрыт навсегда, оснований нет. Корректная формулировка: ущерб от ошибки в движке теперь ограничен по крайней мере задуманной архитектурой.
Практические выводы: обновление и запуск незнакомых ботов
Действия простые и не требуют технической подготовки. Telegram Desktop обновить минимум до 7.4.3, приложение на Android - до сборки 13.2.2. Проверить версию можно в настройках приложения, в разделе с информацией о сборке. Обновление ставить из официального магазина или с сайта Telegram, а не из сторонних сборок.
Отдельно про поведение. По описанию Дурова, для атаки требовался запуск стороннего бота. Это значит, что осторожность с незнакомыми ботами снижает риск, но не отменяет уязвимость в клиенте: виджет открывается одним нажатием, и пользователь не видит, какой JavaScript при этом исполняется.
- Не открывать виджеты ботов, которых вы не запускали осознанно, особенно пришедших по ссылке от третьего лица.
- Обновлять клиент до выхода в чат, а не «когда-нибудь»: окно между релизом с виджетами и релизом с исправлениями в этот раз составило восемь дней.
- На Android проверить, что установлена сборка 13.2.2 или новее: именно там движок вынесен в изолированный процесс.
- Не считать заявление «проблемы нет» техническим аргументом: серверная фильтрация не описана, её границы неизвестны.
Как проверить версию и что делать, если обновления нет
Если магазин приложений не предлагает свежую сборку, разумно временно воздержаться от запуска виджетов и новых ботов. Это неудобно, зато предсказуемо. Устанавливать сборки с форумов и файлопомоек ради «свежего номера версии» точно не стоит: непроверенный APK дает больше доступа к устройству, чем сама уязвимость в движке.
Для тех, кто держит своих ботов с виджетами, полезно держать движок в актуальном состоянии и смотреть журнал исправлений: 6 ноября в v0.6.1 вошло семь правок, 10 ноября в v0.6.4 ещё пять ошибок памяти и вторая ошибка с выполнением кода. Плотность правок говорит о том, что код движка еще молодой.
Что остаётся неясным и как это влияет на оценку рисков
Открытых вопросов больше, чем ответов, и это стоит держать в голове при чтении любых выводов по инциденту.
- Как именно закрыли сценарий на сервере. Дуров этого не объяснил, публичного технического описания нет.
- Почему 13 ноября появились сообщения о неработающих виджетах. Официального подтверждения связи с исправлением нет.
- Сколько из 27 находок аудита с Claude Fable 5.1 подтвердились как реальные уязвимости.
- Полный ли список исправленных ошибок памяти и не осталось ли похожих мест в движке.
Оценка рисков из этого списка выводится простая. Клиент обновить, незнакомые виджеты без необходимости не открывать, заявления вендора проверять по журналу релизов и версиям. Ответственность за слабую изоляцию агентов и клиентского кода - сюжет не только технический: разбор риторики вокруг инцидентов с Hugging Face и выхода агента через DNS показывает, как громкие цитаты подменяют ответ на вопрос, кто отвечает за изоляцию.
История с виджетами дает конкретный урок для всех, кто встраивает чужой код: архитектурная изоляция надежнее списка исправленных багов, а аудит с нейросетями ускоряет поиск, но не отменяет проверку находок руками. Подробности первой недели виджетов, включая реакцию Дурова, собраны в исходном разборе: habr.com/ru/articles/1107658.