Исследователи из Parameter описали цепочку, в которой публично расшаренный разговор ChatGPT позволял выполнить произвольный JavaScript в сессии того, кто открыл ссылку. Публичный шаринг устроен так: автор разговора открывает его для всех, и разговор становится доступен по ссылке вида /share/<id>, которую может открыть любой пользователь. Код исполнялся с веб-идентичностью chatgpt.com и мог обращаться к внутренним API от имени жертвы: читать приватные чаты и файлы, менять аккаунт. OpenAI воспроизвела проблему, раскатала исправление на двух версиях веб-фронтенда и 26 октября подтвердила, что обе версии отклоняют использованные в отчёте межфреймовые сообщения.
Цепочка собралась из четырёх несвязанных компонентов: публичного шаринга, HTML Previewфункция ChatGPT, превращающая HTML-блок в работающую веб-страницу в ограниченном фрейме на другом адресе, интерактивного графа и математической библиотеки MathLiveбиблиотека для отрисовки математики, устанавливающая в странице слушатель сообщений экранной клавиатуры. Ни один из них по отдельности не давал доступа к чужой сессии.
Что именно изменилось
Отдельного «изменения» в продукте здесь два: закрытый вектор атаки и подтверждённое поведение после фикса. Механика эксплойта выглядела так.
Атакующий публикует разговор, жертва открывает публичную ссылку. HTML Preview запускается в ограниченном фрейме на другом адресе и не может читать parent.document основной страницы. Граф в том же разговоре загружает MathLive, и эта загрузка не только рисует график, но и устанавливает в главной странице ChatGPT слушатель сообщений, связанных с экранной клавиатурой библиотеки.
Дальше Preview отправляет два сообщения. Первое подменяет раскладку виртуальной клавиатуры на HTML, второе велит MathLive показать клавиатуру — и та рендерит новую раскладку. При открытии клавиатуры библиотека собирает присланную раскладку в HTML-строку и вставляет её в разметку уже на реальной странице ChatGPT.
Слушатель MathLive принимал сообщения с политикой origin none, то есть с любого веб-адреса, и не требовал, чтобы настройка update-setting пришла из ранее одобренного окна. Обёртка, помечавшая строку как доверенную, не очищала разметку, а пропускала HTML без изменений. Скрипты, вставленные напрямую таким способом, при этом не запускаются — поэтому одного этого шага не хватало.
Как HTML-инъекция стала исполнением кода
Присваивание разметки в HTML-сток страницы могло создать <iframe srcdoc="..."> без атрибута sandbox, и MathLive вставлял такой фрейм в основной документ ChatGPT. Это дало две вещи. Во-первых, srcdoc порождает свежий разбор HTML-документа, где может исполниться внешний скрипт. Во-вторых, фрейм создавала сама страница ChatGPT, поэтому браузер присвоил ему веб-идентичность https://chatgpt.com, и код внутри фрейма получил доступ к родительской странице.
Для загрузки скрипта использовался JSONPприём получения данных через тег script, при котором сервер оборачивает ответ в вызов указанной клиентом функции, поэтому ответ исполняется как скрипт-эндпоинт YouTube oembed с параметром callback: значение callback содержало код globalThis.__marker=31337;void. YouTube возвращал ответ как скрипт: комментарий // API callback, затем исполненный callback, затем объект с ошибкой code: 400 и сообщением про недопустимое имя JSONP-колбэка. Завершение ;void делало добавляемый объект безвредным. YouTube уже был разрешён в script-src-elem действовавшей CSPполитики безопасности содержимого — набора правил, который указывает браузеру, из каких источников можно загружать скрипты и другие ресурсы; script-src-elem задаёт разрешённые источники для тегов script ChatGPT, поэтому браузер загрузил ответ как скрипт и выполнил callback внутри нового фрейма. Политика блокировала inline-код, но не этот путь.
Что делал код от имени жертвы
Поскольку JavaScript исполнялся как chatgpt.com, он работал в том же аутентифицированном контексте, что и обычный фронтенд. Полезная нагрузка читала уже доступную странице информацию о сессии и вызывала API бесед с заголовками Authorization: Bearer <victim token> и ChatGPT-Account-ID: <victim account>. Сначала перечисляла недавние разговоры, затем запрашивала сообщения выбранной приватной беседы. При ответе 403, 404 или 405 на запрос деталей срабатывал фолбэк на legacy-маршрут /backend-api/conversation/<id>?include_full_conversation=true. Перед внешним запросом изображения код очищал ссылки на токен, аккаунт, bootstrap, сессию и заголовки запроса.
Авторы подчёркивают: это прямой аутентифицированный доступ к API, а не скрейпинг видимого текста. Целевая беседа не была связана с вредоносной публикацией и никогда не расшаривалась атакующему, а охват не ограничивался эндпоинтами бесед из доказательства — через те же frontend-API можно было выполнять любые действия от имени аккаунта.
Предыстория
Исследование началось с вопроса: если код Preview заперт на другом сайте, с чем он всё ещё может взаимодействовать. Обмен сообщениями между фреймами — легитимный механизм, который работает, даже когда браузер запрещает фреймам читать страницы друг друга. MathLive не проверял отправителя для нужной настройки и принимал сообщение, менявшее раскладку клавиатуры, а раскладка могла содержать HTML. Граф установил получателя, Preview дал недоверенного отправителя — две несвязанные функции случайно создали путь через песочницу.
Демонстрация прошла несколько итераций. alert доказывал лишь, где исполняется код, но не показывал, почему баг важен. Первая рабочая версия использовала подконтрольный исследователю ChatGPT App как отправителя — жертве пришлось бы сначала подключить или вызвать приложение. При экспорте разговора в публичный доступ вложение оставалось, а граф исчезал; без графа MathLive не загружался и слушатель не устанавливался. Прорыв — поместить оба элемента в одну обычную публичную беседу: настоящий граф инициализирует MathLive в главной странице, а созданный ассистентом HTML Preview позже отправляет два сообщения. Ссылка /share/<id> сохраняла всю цепочку, и жертве достаточно было открыть URL — без установки, вступления в рабочее пространство или аутентификации через поток атакующего.
Почему это сделали и что изменилось после отчёта
Уязвимость подтверждена 13 октября 2026 года на production-коде ChatGPT. 14 октября цепочка воспроизведена через обычный интерфейс, 16–17 октября подтверждены доставка через публичный шаринг и автоматический Preview после жёсткого обновления. 18 октября отчёт ушёл в bug bounty-программупрограмму выплат за найденные уязвимости OpenAI, 19 октября добавлено доказательство раскрытия приватного чата. 21 октября OpenAI воспроизвела проблему и повысила серьёзность с P2 до P1 — то есть перевела её из второй категории приоритета в первую, самую высокую. 22 октября один веб-фронтенд был исправлен, второй ещё обновлялся, 23 октября изменения CSP заблокировали часть цепочки. 26 октября OpenAI подтвердила, что оба фронтенда отклоняют сообщения, закрыла вопрос и выплатила $5 000.
Что это меняет на практике
Для разработчиков LLM-приложений ценность здесь не в истории одного бага, а в наборе правил, которые авторы сформулировали по итогам разбора.
Получатель межфреймовых сообщений должен проверять и адрес отправителя, и конкретное окно, которому дано разрешение, — до принятия команд, меняющих состояние. Межоконные настройки не должны содержать сырой HTML. Динамически создаваемые документы должны оставаться в песочнице, если на их содержимое может влиять недоверенный фрейм. CSP — резервный контроль, а не единственный барьер после того, как управляемый атакующим HTML достиг основного сайта.
Влияние JavaScript, исполняющегося в контексте того же сайта (same-originкогда код получает права страницы, с которой загружен, и может читать её данные и действовать от её имени), нужно измерять в аутентифицированном контексте жертвы: код, исполнявшийся как chatgpt.com, дотягивался до тех же приватных данных и действий аккаунта, что и обычный фронтенд. И проверять стоит не отдельные функции, а швы между ними: цепочка жила на стыке публичного шаринга, Preview, MathLive, создания фреймов и CSP.
Ограничения и открытые вопросы
Для успеха должны были совпасть несколько условий. Граф и Preview загружались независимо, поэтому полезная нагрузка могла отправить сообщение до готовности MathLive; финальный носитель обрабатывал эту гонку, повторяя те же два сообщения в течение ограниченного периода, а guard на главной странице обеспечивал однократное выполнение проверки приватной переписки и исходящего запроса. Без графа вложение могло отправлять сообщения, но их никто не обрабатывал.
В наиболее стабильном варианте жертве нужно было открыть публикацию и один раз нажать кнопку Preview. Позже HTML-блок пометили как previewableпомеченный так, чтобы ChatGPT открывал его автоматически, — тогда единственным действием оставалось открытие ссылки. Автоматический путь воспроизведён трижды в отдельном аккаунте, включая после жёсткого обновления, с появлением alert примерно через 4,5 секунды.
MathLive принял меняющее состояние сообщение без доказательства, какой фрейм его отправил, а формат сообщения позволял передавать сырой HTML как данные раскладки клавиатуры.