研究员披露 ChatGPT 曾存在的漏洞及利用链
We Took over ChatGPT Accounts
安全团队 armcat 披露了在 2026 年 10 月发现并协助 OpenAI 修复的一个 ChatGPT 漏洞。攻击者可通过构造包含特定预览与交互组件的公开分享链接,在受害者访问时执行未加沙箱限制的脚本,从而读取私密对话并调用认证 API。OpenAI 已于 10 月 26 日完成修复并将该报告评定为 P1 级别。
安全研究员详细剖析了 ChatGPT 此前存在的漏洞及利用链,揭示了多个独立功能组合后带来的安全风险及修复方式。
2026年10月,我们发现了一种方法,可以将普通的公开 ChatGPT 分享链接转变为由攻击者控制的 JavaScript 代码,并在任何打开该链接的已登录用户会话中运行。
攻击者可以创建一个恶意对话,使用 ChatGPT 的正常分享功能将其发布,并将链接发送给另一位用户。当受害者打开该分享时,有效负载作为 chatgpt.com 执行,并有权访问受害者的已认证网页会话。
我们确认,这段代码可以枚举受害者的对话列表,检索从未与攻击者分享过的私密对话中的内容,并将该对话中的数据发送到外部服务器。该有效负载不仅限于修改页面或显示警报——它还可以冒充受害者直接调用 ChatGPT 的已认证 API,这实质上允许我们接管受害者的 ChatGPT 账户。
受害者无需安装应用、加入项目、粘贴代码或打开开发者工具。打开分享链接是唯一需要的操作。
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 插入的 script 标签不会自动运行。
下一步是使用 srcdoc 创建一个 iframe,这是一个浏览器功能,允许嵌入的框架直接在其属性中承载 HTML:
<iframe srcdoc="..."></iframe>MathLive 将这个框架插入到主 ChatGPT 文档中,且没有使用 sandbox 属性。这为我们提供了两个有用的属性。首先,srcdoc 创建了一个全新的文档,外部脚本可以在其中执行。其次,由于主 ChatGPT 页面创建了该框架,浏览器赋予了它 https://chatgpt.com 网络身份。因此,新框架内的代码可以访问父页面。
通过 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 追加的对象变得无害。由于 YouTube 已经被 ChatGPT 的 CSP 允许,浏览器将此响应作为脚本加载,并在新框架内运行了我们的回调。
至此,我们已经有了以 chatgpt.com 身份运行的任意 JavaScript。最初的 Preview 仍然隔离在其独立的网络地址上;我们通过说服主页面中的受信组件为我们创建一个新的、未沙盒化的文档,从而跨越了边界。
将一个实验室漏洞转化为一个公开链接
到目前为止,这是一个有用的沙盒逃逸,但最初的工作版本并不是一个令人信服的现实世界攻击。它使用了一个由研究人员控制的 ChatGPT App 作为发送方。受害者需要先连接或调用该 App。
我们接下来尝试通过本地 HTML 附件交付有效载荷。ChatGPT 在同一个沙盒化的 Preview 内部打开了附件,因此它能够发送恶意的 MathLive 消息。
然而,当我们将对话导出为公开分享时,附件保留了下来,但早期的图表却没有。图表至关重要,因为它加载了 MathLive 并在主页面中安装了易受攻击的消息侦听器。没有它,附件仍然可以发送消息,但没有任何东西在监听或处理它们。
突破口是将这两部分直接放入一个普通的公开对话中:
- 一个真正的图表在主页面中初始化了 MathLive。
- 随后由助手编写的 HTML Preview 发送了这两条消息。
由此产生的 /share/<id> 链接为其他用户保留了完整的链条。攻击者可以通过发送任何共享 ChatGPT 对话的相同方式来传递它——通过电子邮件、聊天、支持工单或社交帖子。受害者不需要安装任何东西、加入攻击者的工作区,也不需要通过攻击者控制的流程进行身份验证。他们只需访问攻击者共享的 ChatGPT URL 即可。
我们观察到该交互的两种版本。在最稳定的复现中,受害者打开共享并点击了 ChatGPT 正常的 Preview 按钮一次。我们后来将 HTML 块标记为可预览,以便 ChatGPT 自动打开它。在该版本中,打开链接是受害者唯一的点击操作。我们在另一个账户中三次复现了自动执行路径(包括在强制刷新之后),警报在大约 4.5 秒后出现。
图表和 Preview 是独立加载的,因此有效载荷有时可能会在 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 修复了什么
Preview 沙箱本身是工作的。Preview 内部的代码无法直接读取 ChatGPT 页面。问题在于主页面中的另一个组件愿意接受其指令。
三个设计决策将该失误演变成了完整的漏洞利用:
- MathLive 接受了一条改变状态的消息,却没有验证是哪个框架发送了它。
- 消息格式允许将原始 HTML 作为键盘布局数据提供。
- 该 HTML 可以创建一个继承了
chatgpt.com身份的非沙箱化文档。
YouTube 的响应只是针对 CSP 的最后一种变通方法,而不是根本原因。仅仅阻止该端点会移除通往 JavaScript 的一条路径,但仍会留下不受信任的预览,能够将 HTML 插入到 ChatGPT 中。
OpenAI 重现了 JavaScript 执行和私密对话证明。此修复程序分两版推送到了 ChatGPT 的网页前端。在推送期间,CSP 的更改阻止了原始链的一部分,而其中一个版本仍需更新。10 月 26 日,OpenAI 确认这两个版本现在都拒绝了报告所使用的跨框架 MathLive 消息,并标记该问题已解决。
防范方面的经验教训相当简单:
- 消息接收者应同时验证发送方的网址和已被授予权限的具体窗口。
- 跨窗口设置不应包含原始 HTML。
- 当动态创建的文档内容可能受不受信任的框架影响时,应保持沙盒化。
- CSP 应当是一项后备控制措施,而不是攻击者控制的 HTML 到达主站点后的唯一屏障。
这个漏洞之所以有趣,是因为每一个单独的功能看起来都是合理的。预览是隔离的。图表可以加载数学键盘。框架可以交换消息。CSP 允许主要的媒体提供商。公开对话可以共享。只有当所有这些功能组合在一个页面中时,漏洞才会出现。
时间线
| 日期 | 事件 |
|---|---|
| 2026 年 10 月 13 日 | 确认了针对 ChatGPT 生产代码的 MathLive 消息到 HTML 路径。 |
| 10 月 14 日 | 通过正常的 UI 重现了攻击链,并将修复警报升级为任意 JavaScript。 |
| 10 月 16 日 | 确认了只需点击一次预览即可在不同账户和工作区之间交付公开分享。 |
| 10 月 17 日 | 确认了来自分享导航的自动预览,并在硬刷新后重现了该问题。 |
| 10 月 18 日 | 向 OpenAI 的漏洞赏金计划提交了报告。 |
| 10 月 19 日 | 添加了受控的私密聊天披露证明。 |
| 10 月 21 日 | OpenAI 重现了该问题;严重程度从最初的 P2 分诊提升至 P1。 |
| 10 月 22 日 | OpenAI 报告称一个网页前端已修复,而第二个前端仍在更新中。 |
| 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,它阻止了内联代码,但允许在 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身份运行,它就能访问正常前端可用的相同私密数据和账户操作,而不仅仅是显示一个弹窗。 - 跨功能测试至关重要。这个攻击链存在于公共共享、Preview、MathLive、框架创建和 CSP 之间的缝隙中,而不是存在于单独看任何单一功能中。
更广泛的教训是,安全边界必须在整个产品中保持有效,而不仅仅是在单个组件内部。任何公开的或可共享的表面都应被视为攻击者控制的输入,每个跨框架消息都应进行身份验证,且不受信任的标记在其整个渲染过程中都应保持沙盒化。
来源:Hacker News-首页 · parameter.ai