Do the math: How we took over ChatGPT accounts
本文为原文前 6,000 字符的节选翻译,完整内容请查看原文。
Title: Do the math: How we took over ChatGPT accounts
通过绕过 ChatGPT 的沙盒和内容安全策略,我们的研究团队只需向用户发送一个链接,就能接管任何人的账户。
In October 2026, we found a way to turn an ordinary public ChatGPT share into attacker-controlled JavaScript running inside the signed-in session of whoever opened it.
2026 年 10 月,我们发现了一种方法,可以将普通的 ChatGPT 公开分享链接转化为攻击者可控的 JavaScript 代码,并在任何打开该链接的用户已登录会话中运行。
An attacker could create a malicious conversation, publish it using ChatGPT’s normal sharing feature, and send the link to another user. When the victim opened the share, the payload executed as chatgpt.com with access to the victim’s authenticated web session.
攻击者可以创建一个恶意对话,使用 ChatGPT 的常规分享功能发布它,并将链接发送给其他用户。当受害者打开该分享时,有效载荷会以 chatgpt.com 的身份执行,并获得对受害者已认证 Web 会话的访问权限。
We confirmed that this code could enumerate the victim’s conversation list, retrieve content from a private conversation that had never been shared with the attacker, and send data from that conversation to an external server. The payload was not limited to modifying the page or displaying an alert—it could directly call ChatGPT’s authenticated APIs as the victim, essentially allowing us to take over the victim’s ChatGPT account.
我们证实,这段代码可以枚举受害者的对话列表,检索从未与攻击者共享过的私人对话内容,并将这些对话数据发送到外部服务器。该有效载荷不仅限于修改页面或显示警告,它还可以直接以受害者的身份调用 ChatGPT 的已认证 API,从而使我们能够接管受害者的 ChatGPT 账户。
The victim did not need to install an App, join a Project, paste code, or open developer tools. Opening the shared link was the only required action.
受害者无需安装应用程序、加入项目、粘贴代码或打开开发者工具。打开分享链接是唯一需要的操作。
OpenAI fixed the issue on October 26 and classified the report as P1.
OpenAI 于 10 月 26 日修复了该问题,并将此报告归类为 P1 级。
The chain started with two features that looked harmless on their own: a graph and an HTML Preview.
这一攻击链始于两个单独看起来并无害的功能:图表和 HTML 预览。
The question that started this research was simple: if Preview code was boxed into a different website, what could it still talk to?
这项研究最初的问题很简单:如果预览代码被封装在另一个网站中,它还能与什么进行通信?
ChatGPT can turn an HTML code block into a small working webpage using its Preview feature. Since that page may contain arbitrary code, ChatGPT sensibly runs it inside a restricted frame on a different web address. The Preview can run JavaScript, but the browser treats it as a different website, so it cannot directly inspect the signed-in ChatGPT page around it.
ChatGPT 可以利用其预览功能将 HTML 代码块转换为一个小型的功能性网页。由于该页面可能包含任意代码,ChatGPT 明智地将其运行在不同网址的受限框架内。预览功能可以运行 JavaScript,但浏览器将其视为不同的网站,因此它无法直接检查周围已登录的 ChatGPT 页面。
At first, this isolation appeared to work exactly as intended. Code inside the Preview could not read parent.document, which is the main ChatGPT page. That should have been the end of it.
起初,这种隔离看起来完全符合预期。预览内部的代码无法读取 parent.document(即 ChatGPT 主页面)。本应到此为止。
The conversation also contained a normal interactive graph. Behind the scenes, the graph loaded a math library called MathLive. Loading it did more than draw the graph: it also installed a listener in the main ChatGPT page for messages related to MathLive’s on-screen keyboard.
对话中还包含一个普通的交互式图表。在后台,该图表加载了一个名为 MathLive 的数学库。加载它不仅仅是为了绘制图表:它还在 ChatGPT 主页面中安装了一个监听器,用于接收与 MathLive 屏幕键盘相关的消息。
Browsers call the relevant API postMessage. It is a legitimate way for two frames to exchange information even when the browser prevents them from reading each other’s pages. You can think of it as a mail slot: the restricted Preview cannot walk into the main page, but it can still drop a message through the slot.
浏览器调用相关的 API 称为 postMessage。即使浏览器阻止两个框架读取彼此的页面,这也是它们交换信息的合法方式。你可以把它想象成一个投信口:受限的预览页面无法进入主页面,但仍然可以通过投信口投递消息。
That is safe only if the receiver checks who sent the message.
只有当接收方检查消息发送者时,这才是安全的。
MathLive did not make that check for the setting we were interested in. Its listener accepted a message that changed the virtual keyboard layout, and that layout was allowed to contain HTML. The graph had installed the receiver; the Preview gave us an untrusted sender. Two unrelated features had accidentally created a path across the sandbox.
MathLive 在我们感兴趣的设置上没有进行这种检查。它的监听器接受了一条更改虚拟键盘布局的消息,并且该布局允许包含 HTML。图表安装了接收器,而预览功能为我们提供了不受信任的发送者。两个不相关的功能意外地在沙盒中开辟了一条路径。
The Preview sent two messages. The first replaced a virtual keyboard layout with our HTML. The second told MathLive to show the keyboard, causing it to render the new layout:
预览发送了两条消息。第一条用我们的 HTML 替换了虚拟键盘布局。第二条告诉 MathLive 显示键盘,导致它渲染出新的布局:
MathLive recognized the public message type, but its top-level controller used an origin policy of none, which meant it accepted messages from any web address. It also did not require the update-setting message to come from a previously approved window. Anyone who could reach the listener and knew the message format could replace the layout.
MathLive 识别了该公共消息类型,但其顶层控制器使用了“无”来源策略,这意味着它接受来自任何网址的消息。它也不要求 update-setting 消息必须来自预先批准的窗口。任何能够触及该监听器并了解消息格式的人都可以替换布局。
When the keyboard opened, MathLive joined the supplied layout into an HTML string and assigned it to innerHTML in the real ChatGPT page. A helper named createHTML initially looked like it might sanitize the value. It did not; it marked the string as trusted and passed our HTML through unchanged.
当键盘打开时,MathLive 将提供的布局合并为一个 HTML 字符串,并将其分配给真实 ChatGPT 页面中的 innerHTML。一个名为 createHTML 的辅助函数起初看起来可能会对该值进行清理。但它并没有这样做;它将该字符串标记为受信任,并原封不动地传递了我们的 HTML。
This was exciting, but we still did not have JavaScript execution. Script tags inserted through innerHTML do not automatically run.
这令人兴奋,但我们仍然没有实现 JavaScript 执行。通过 innerHTML 插入的脚本标签不会自动运行。
The next step was an iframe using srcdoc, a browser feature that lets an embedded frame carry its HTML directly in an attribute:
下一步是使用 srcdoc 的 iframe,这是一种浏览器功能,允许嵌入的框架直接在属性中携带其 HTML:
MathLive inserted this frame into the main ChatGPT document without a sandbox attribute. That gave us two useful properties. First, srcdoc created a fresh document where an external script could execute. Second, because the main ChatGPT page created the frame, the browser gave it the https://chatgpt.com web identity. Code inside the new frame could therefore access the parent page.
MathLive 在没有 sandbox 属性的情况下将此框架插入到 ChatGPT 主文档中。这为我们提供了两个有用的特性。首先,srcdoc 创建了一个全新的文档,外部脚本可以在其中执行。其次,由于该框架是由 ChatGPT 主页面创建的,浏览器赋予了它 https://chatgpt.com 的 Web 身份。因此,新框架内的代码可以访问父页面。
ChatGPT’s Content Security Policy, or CSP, stopped us again. CSP controls which scripts a page may load, and the policy correctly blocked inline JavaScript. It did, however, allow scripts from YouTube.
ChatGPT 的内容安全策略(CSP)再次阻碍了我们。CSP 控制页面可以加载哪些脚本,该策略正确地阻止了内联 JavaScript。然而,它确实允许来自 YouTube 的脚本。
YouTube’s oEmbed endpoint supported a JSONP-style callback parameter. JSONP responses are JavaScript rather than ordinary JSON. The endpoint tried to reject an invalid callback, but it placed the callback into the response before returning its validation error:
YouTube 的 oEmbed 端点支持 JSONP 风格的回调参数。JSONP 响应是 JavaScript 而不是普通的 JSON。该端点试图拒绝无效的回调,但在返回验证错误之前,它将回调放入了响应中:
The response began like this:
响应开头如下:
Ending our callback with ;void made the object appended by YouTube harmless. Since YouTube was already allowed by ChatGPT’s CSP, the browser loaded this response as a script and ran our callback inside the new frame.
通过以 ;void 结束我们的回调,使得 YouTube 附加的对象变得无害。由于 YouTube 已经被 ChatGPT 的 CSP 允许,浏览器将此响应作为脚本加载,并在新框架内运行了我们的回调。
We now had arbitrary JavaScript running as chatgpt.com. The original Preview was still isolated on its separate web address; we had crossed the boundary by convincing a trusted component in the main page to create a new, unsandboxed document for us.
现在我们有了以 chatgpt.com 身份运行的任意 JavaScript。最初的预览页面仍然被隔离在其独立的网址上;我们通过说服主页面中的一个受信任组件为我们创建一个新的、未沙盒化的文档,从而跨越了边界。
So far, this was a useful sandbox escape, but the first working version was not a convincing real-world attack. It used a researcher-controlled ChatGPT App as the sender. A victim would have needed to connect or invoke that App first.
到目前为止,这是一个有用的沙盒逃逸,但第一个可行版本并不是一个令人信服的现实攻击。它使用了研究人员控制的 ChatGPT 应用程序作为发送者。受害者需要先连接或调用该应用程序。
We next tried delivering the payload through a native HTML attachment. ChatGPT opened the attachment inside the same sandboxed Preview, so it could send the malicious MathLive messages.
接下来,我们尝试通过原生 HTML 附件传递有效载荷。ChatGPT 在同一个沙盒预览中打开了该附件,因此它可以发送恶意的 MathLive 消息。
However, when we exported the conversation as a public share, the attachment remained but the earlier graph d
然而,当我们以公开分享的形式导出对话时,附件保留了下来,但之前的图表 d