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

此时,公开分享可以显示一个被浏览器识别为来自 chatgpt.com 的警报,并可以在主页面中添加一个横幅。这证明了我们的代码在哪里运行。但它还没有展示这个漏洞为什么重要。


