2026年10月,我们发现了一种方法,可以将一个普通的公开ChatGPT分享变成由攻击者控制的JavaScript,在打开该分享的任何人的已登录会话中运行。
攻击者可以创建一个恶意对话,使用ChatGPT的常规分享功能发布它,并将链接发送给另一个用户。当受害者打开分享时,有效载荷以chatgpt.com身份执行,并能够访问受害者的已认证Web会话。
我们确认,这段代码可以枚举受害者的对话列表,从从未与攻击者分享过的私人对话中检索内容,并将该对话中的数据发送到外部服务器。该有效载荷不仅限于修改页面或显示警报——它可以直接以受害者身份调用ChatGPT的已认证API,本质上允许我们接管受害者的ChatGPT账户。
受害者无需安装App、加入Project、粘贴代码或打开开发者工具。打开共享链接是唯一需要的操作。
OpenAI于10月26日修复了该问题,并将报告归类为P1。
这条攻击链始于两个单独看起来无害的功能:一个图表和一个HTML预览。
flowchart TD
A["Attacker publishes a ChatGPT conversation"] --> B["Victim opens the public share"]
B --> C["HTML Preview crosses into the main chatgpt.com page"]
C --> D["JavaScript runs in the victim's signed-in session"]
D --> E["Any action on chatgpt.com as the victim:<br/>read private chats and files, change the account"]
技术详解
始于一个图表和一个预览
启动这项研究的问题很简单:如果预览代码被限制在不同的网站中,它还能与什么通信?
ChatGPT可以使用其预览功能将HTML代码块变成一个小型可工作的网页。由于该页面可能包含任意代码,ChatGPT明智地在不同网址的受限框架内运行它。预览可以运行JavaScript,但浏览器将其视为不同的网站,因此它无法直接检查周围已登录的ChatGPT页面。
起初,这种隔离似乎完全按预期工作。预览内的代码无法读取parent.document,即主要的ChatGPT页面。事情本应到此为止。
对话中还包含一个正常的交互式图表。在后台,该图表加载了一个名为MathLive的数学库。加载它不仅仅是绘制图表:它还在主ChatGPT页面中安装了一个监听器,用于处理与MathLive屏幕键盘相关的消息。
浏览器将相关API称为postMessage。这是一种合法的方式,让两个框架交换信息,即使浏览器阻止它们读取彼此的页面。你可以把它想象成一个投信口:受限的预览无法走进主页面,但它仍然可以通过投信口投递消息。
只有当接收方检查谁发送了消息时,这才是安全的。
对于我们感兴趣的设置,MathLive没有进行该检查。它的监听器接受了一条更改虚拟键盘布局的消息,而该布局允许包含HTML。图表安装了接收器;预览给了我们一个不受信任的发送者。两个不相关的功能意外地创造了一条跨越沙箱的路径。
本不应起作用的消息
预览发送了两条消息。第一条用我们的HTML替换了虚拟键盘布局。第二条告诉MathLive显示键盘,导致它渲染新布局:
top.postMessage({
type: "mathlive#virtual-keyboard-message",
action: "update-setting",
layouts: [{
label: "Compatibility",
layers: [{ id: "compatibility", markup }]
}]
}, "*");
top.postMessage({
type: "mathlive#virtual-keyboard-message",
action: "show",
animate: false
}, "*");
MathLive识别了公共消息类型,但其顶层控制器使用了none的源策略,这意味着它接受来自任何网址的消息。它也不要求update-setting消息来自先前批准的窗口。任何能访问监听器并知道消息格式的人都可以替换布局。
当键盘打开时,MathLive将提供的布局连接成HTML字符串,并将其赋值给真实ChatGPT页面中的innerHTML。一个名为createHTML的辅助函数最初看起来可能会清理该值。但它没有;它将字符串标记为可信,并原样传递了我们的HTML。
这很令人兴奋,但我们仍然没有JavaScript执行权限。通过innerHTML插入的脚本标签不会自动运行。
下一步是使用srcdoc的iframe,这是一种浏览器功能,允许嵌入框架直接在其属性中携带HTML:
<iframe srcdoc="..."></iframe>
MathLive将此框架插入主ChatGPT文档,但没有sandbox属性。这给了我们两个有用的特性。首先,srcdoc创建了一个新文档,外部脚本可以在其中执行。其次,由于主ChatGPT页面创建了该框架,浏览器赋予了它https://chatgpt.com的Web身份。因此,新框架内的代码可以访问父页面。
利用YouTube JSONP绕过CSP
ChatGPT的内容安全策略(CSP)再次阻止了我们。CSP控制页面可以加载哪些脚本,该策略正确地阻止了内联JavaScript。然而,它确实允许来自YouTube的脚本。
YouTube的oEmbed端点支持JSONP风格的回调参数。JSONP响应是JavaScript而不是普通JSON。该端点试图拒绝无效回调,但它在返回验证错误之前将回调放入了响应中:
https://www.youtube.com/oembed?callback=globalThis.__marker=31337;void
响应开头如下:
// API callback
globalThis.__marker=31337;void({
"error": {
"code": 400,
"message": "Invalid JSONP callback name: ..."
}
});
用;void结束我们的回调使YouTube附加的对象无害。由于ChatGPT的CSP已经允许YouTube,浏览器将此响应作为脚本加载,并在新框架内运行我们的回调。
现在我们有以chatgpt.com身份运行的任意JavaScript。原始预览仍然隔离在其单独的网址上;我们通过说服主页面中的可信组件为我们创建一个新的、未沙箱化的文档,跨越了边界。
将实验室漏洞变成一个公开链接
到目前为止,这是一个有用的沙箱逃逸,但第一个可工作的版本并不是一个有说服力的现实世界攻击。它使用研究人员控制的ChatGPT App作为发送者。受害者需要先连接或调用该App。
接下来,我们尝试通过原生HTML附件传递有效载荷。ChatGPT在同一沙箱预览中打开附件,因此它可以发送恶意的MathLive消息。
然而,当我们将对话导出为公开分享时,附件仍然保留,但之前的图表却没有了。图表至关重要,因为它加载了 MathLive 并在主页面中安装了存在漏洞的消息监听器。没有它,附件仍然可以发送消息,但没有任何东西在监听或处理这些消息。
突破点在于将两部分直接放入一个普通的公开对话中:
- 一个真实的图表在主页面中初始化了 MathLive。
- 随后由助手编写的 HTML 预览发送了这两条消息。
生成的 /share/<id> 链接为另一个用户保留了完整的链条。攻击者可以像发送任何共享的 ChatGPT 对话一样传递它——通过电子邮件、聊天、支持工单或社交帖子。受害者无需安装任何东西、加入攻击者的工作区,或通过攻击者控制的流程进行身份验证。他们只需访问攻击者共享的 ChatGPT URL。
我们观察到了两种交互版本。在最稳定的复现中,受害者打开分享并点击一次 ChatGPT 正常的 预览 按钮。后来我们将 HTML 块标记为可预览,以便 ChatGPT 自动打开它。在那个版本中,打开链接是受害者唯一的点击。我们在一个单独的账户中三次复现了自动路径,包括在硬刷新之后,警报在大约 4.5 秒后出现。
图表和预览独立加载,因此有效载荷有时可能在 MathLive 就绪之前发送消息。最终的载体通过在一段有限时间内重试相同的两条消息来处理这种竞争。主页面中的一个守卫确保私有对话证明和出站请求只运行一次。

此时,公开分享可以显示一个被浏览器识别为来自 chatgpt.com 的警报,并可以向主页面添加一个横幅。这证明了我们的代码在哪里运行。但它还没有展示这个漏洞为何重要。
仅一个警报还不够
此时,显示警报是该漏洞利用能做的最无趣的事情。JavaScript 以 chatgpt.com 身份运行,这意味着它可以像正常 Web 应用程序一样,使用相同的已认证上下文与 ChatGPT 交互。
有效载荷读取了页面已经可用的会话信息,并利用它调用 ChatGPT 的对话 API。首先,它枚举了受害者最近的对话。然后,它可以请求所选私有对话的消息数据:
GET /backend-api/conversations?...&limit=20&order=updated&offset=0
GET /backend-api/conversations/<conversation-id>?num_turns=2
这是直接的已认证 API 访问,而不是抓取恰好显示在屏幕上的文本。目标对话与恶意分享无关,且从未与攻击者共享。
一旦响应可供注入的 JavaScript 使用,将数据发送到站外就很简单了。有效载荷创建了一个指向我们控制的服务器的普通图像请求。
我们故意只将响应的一小部分发送到站外。注入的代码已经收到了已认证的对话响应,并且可以读取其内容,因此该漏洞并不局限于那个值。

结果是一个实用的跨用户数据窃取原语。一个恶意的公开对话可以在另一个用户已登录的 ChatGPT 会话中执行,枚举私有对话,检索私有消息内容,并将选定的数据发送到攻击者控制的服务器。
由于代码以 Web 应用程序的已认证访问权限运行,其影响范围本质上并不局限于我们证明中使用的对话端点。通过 ChatGPT 已认证前端 API 暴露的其他数据和操作也处于同一安全边界内。本质上,我们可以代表受害者的 ChatGPT 账户执行任何操作。
出了什么问题以及 OpenAI 修复了什么
预览沙箱本身是有效的。预览内的代码无法直接读取 ChatGPT 页面。问题在于主页面中的另一个组件愿意接受其指令。
三个设计决策将那个错误变成了完整的漏洞利用:
- MathLive 接受了一条改变状态的消息,却没有证明是哪个框架发送的。
- 消息格式允许将原始 HTML 作为键盘布局数据提供。
- 该 HTML 可以创建一个未沙箱化的文档,该文档继承了
chatgpt.com身份。
YouTube 响应是 CSP 的最终变通方法,而不是根本原因。仅阻止该端点会移除一条通往 JavaScript 的路径,但仍会留下一个不受信任的预览能够将 HTML 插入 ChatGPT。
OpenAI 复现了 JavaScript 执行和私有对话证明。该修复在两个版本的 ChatGPT Web 前端中推出。在推出期间,CSP 更改阻止了原始链条的一部分,而一个版本仍需要更新。10 月 26 日,OpenAI 确认两个版本现在都拒绝报告中使用的跨框架 MathLive 消息,并将该问题标记为已解决。
防御教训相当简单:
- 消息接收者应验证发送者的网址和被授予权限的确切窗口。
- 跨窗口设置不应包含原始 HTML。
- 动态创建的文档在其内容可能受到不受信任框架影响时应保持沙箱化。
- CSP 应作为备份控制,而不是攻击者控制的 HTML 到达主站点后的唯一屏障。
这个漏洞很有趣,因为每个单独的功能看起来都合理。预览是隔离的。图表可以加载数学键盘。框架可以交换消息。CSP 允许一个主要媒体提供商。公开对话可以共享。只有当所有这些功能组合在一个页面中时,漏洞才出现。
时间线
| 日期 | 事件 |
|---|---|
| 2026 年 10 月 13 日 | 针对 ChatGPT 的生产代码确认了 MathLive 消息到 HTML 的路径。 |
| 10 月 14 日 | 通过正常 UI 复现了该链条,并将固定 alert 升级为任意 JavaScript。 |
| 10 月 16 日 | 确认了在不同账户和工作区之间,只需一次 Preview 点击即可实现公开分享投递。 |
| 10 月 17 日 | 确认了从分享导航触发的自动 Preview,并在强制刷新后复现。 |
| 10 月 18 日 | 向 OpenAI 的漏洞赏金计划提交了报告。 |
| 10 月 19 日 | 补充了受控的私密聊天泄露证明。 |
| 10 月 21 日 | OpenAI 复现了该问题;严重性从最初的 P2 分诊提升至 P1。 |
| 10 月 22 日 | OpenAI 报告称一个 Web 前端已修复,而第二个仍在更新中。 |
| 10 月 23 日 | CSP 变更阻断了部分链条,但仍有一个前端更新尚未完成。 |
| 10 月 26 日 | OpenAI 确认两个前端均拒绝了所报告的消息,解决了该问题,并发放了总计 5,000 美元的赏金。 |
技术说明
上文主故事包含了完整的利用链。以下细节面向希望更仔细地跟进生产代码和载荷变换的读者。
相关的去混淆控制流等价于:
window.addEventListener("message", (event) => {
if (event.data?.type !== "mathlive#virtual-keyboard-message") return;
if (!validateOrigin(event.origin, this.originValidator)) return;
this.handleMessage(event.data, event.source);
});
if (message.action === "update-setting" && message.layouts) {
this.layouts = message.layouts;
}
布局规范化保留了 layouts[].layers[].markup。显示键盘最终执行了等价于以下内容的操作:
keyboardContainer.innerHTML = MathfieldElement.createHTML(keyboardHTML);
createHTML 充当的是恒等或 Trusted Types 包装器,而非净化器。Trusted Types 可以限制谁向 HTML 接收器赋值,但恒等策略并不能使攻击者控制的字符串变得安全。
解析器与 CSP 转换。 innerHTML 操作可以创建 iframe,但以这种方式直接插入的脚本仍会保持惰性。未加沙箱的 srcdoc 导致了一次全新的 HTML 文档解析,并继承了嵌入的 ChatGPT 源。该文档还继承了测试期间捕获的有效 CSP,该 CSP 阻止了内联代码,但允许 script-src-elem 中的 www.youtube.com。
oEmbed 响应返回了 HTTP 200,带有 Content-Type: text/javascript; charset=UTF-8 和 X-Content-Type-Options: nosniff;未观察到 Cross-Origin-Resource-Policy 头。Google 重写了回调中的尖括号,因此载荷使用了经典的 function 语法而非箭头函数。以 ;void 结束程序消耗了端点追加的错误对象。
对于私密对话证明,函数通过 toString() 转换为源码,双引号转换为单引号,空白被折叠。URLSearchParams 随后将其编码进回调,脚本 URL 在 <script src> 内被 HTML 转义,整个元素又被再次转义进 iframe 的 srcdoc 属性。最终回调为 5,533 个字符;YouTube URL 为 7,309 个字符;脚本元素为 7,337 个字符;最终 iframe 标记为 7,390 个字符。
已认证读取。 同源框架从父文档解析了 #client-bootstrap,提取了会话访问令牌和账户 ID,并在两者都存在时要求令牌中的账户 ID 与会话一致。它将这些用于内存中的请求头:
Authorization: Bearer <victim token>
ChatGPT-Account-ID: <victim account>
Accept: application/json
主要请求为:
GET /backend-api/conversations?exclude_conversation_origin=tpp&expand=false&is_archived=false&is_starred=false&limit=20&order=updated&offset=0
GET /backend-api/conversations/<id>?num_turns=2&include_has_versions=true
只有在详情请求返回 403、404 或 405 时,代码才会尝试旧版 /backend-api/conversation/<id>?include_full_conversation=true 路由。验证仅传输了响应中一个小的选定值。在发起外部图片请求之前,它清除了对令牌、账户、引导、会话和请求头的引用。
关键要点
- 隔离的强度取决于跨越它的每一座桥梁。当特权监听器信任来自错误发送者的消息时,沙箱化框架仍可能攻陷其父级。
postMessage接收器在接受改变状态的指令之前,必须同时认证event.origin和event.source。- HTML 注入可以通过全新的、未加沙箱的文档以及允许的第三方脚本端点,转变为 JavaScript 执行。CSP 是有价值的纵深防御,但它无法使不安全的渲染路径变得安全。
- 同源 JavaScript 的影响必须在受害者的已认证上下文中衡量。一旦代码以
chatgpt.com身份运行,它就能触及正常前端可访问的相同私密数据和账户操作——而不仅仅是显示一个 alert。 - 跨功能测试很重要。该链条存在于公开分享、Preview、MathLive、框架创建和 CSP 之间的接缝中,而非任何单一功能单独审视时。
更广泛的教训是,安全边界必须在整个产品中成立,而不仅仅是在各个组件内部。任何公开或可分享的表面都应被视为攻击者控制的输入,每条跨框架消息都应经过认证,不可信标记应在整个渲染路径中保持沙箱化。
