做数学:我们如何接管ChatGPT账户
通过绕过ChatGPT的沙盒和内容安全政策,我们的研究团队能够简单地通过发送链接来接管任何人的账户。
2026年10月,我们找到了一种方法,将一个普通的公用ChatGPT股份转变为由攻击者控制的JavaScript运行在谁打开的签名会话内。
攻击者可以创建恶意对话,使用ChatGPT的正常共享功能来发布,并将链接发送给另一个用户。 当受害者打开股权时,有效载荷作为chatgpt.com执行,并访问受害者经认证的网页会话。
我们确认这个代码可以列出受害者的谈话列表,从从未与攻击者共享的私人谈话中取回内容,并将对话中的数据发送到外部服务器。 有效载荷并不限于修改页面或显示警报——它可以直接将ChatGPT经认证的API称为受害者,基本上允许我们接管受害者的ChatGPT帐户。
受害人不需要安装App、加入项目、粘贴码或打开开发工具。 开放共同联系是唯一需要采取的行动。
OpenAI于10月26日确定发行,并将报告分类为P1.
链条开始有两个功能,它们自己看起来是无害的:一个图表和一个HTML预览。
技术通道
它从一个图表和一个预览开始
启动这一研究的问题很简单:如果将预览代码装入另一个网站,它还能和什么交谈?
ChatGPT可以使用它的"预览"功能将一个HTML代码块变成一个小的工作网页。 由于该页面可能包含任意代码,ChatGPT在不同的网页地址上敏锐地在一个受限制的框架内运行。 "预览"可以运行JavaScript,但浏览器视之为不同的网站,因此无法直接检查周围已签名的ChatGPT页面。
起初,这种孤立似乎完全如所愿。 预览中的代码无法读取父代码。 文档,这是 ChatGPT 主页。 这应该是结束的。
对话中还包含一个正常的互动图。 在幕后,图上加载了一个叫"MathLive"的数学库。 装入它所做的不只是绘制出图表:它还在主的ChatGPT页面上为与MathLive在屏幕上键盘相关的消息安装了收听器。
浏览器调用相关的API后Message. 即使浏览器阻止了两个帧相互阅读页面,这也是两个帧交换信息的合法方式。 您可以把它看作是一个邮件插槽:受限制的预览不能走入主页,但它仍然可以通过插槽来放出一个消息。
只有接收者检查发送信息的人才安全
MathLive没有检查我们感兴趣的环境它的收听器接受了更改了虚拟键盘布局的消息,这种布局被允许包含HTML. 该图已经安装了接收器;预览给了我们一个不信任的发送器。 两个无关的特性意外地创造了一条穿过沙盒的路径。
本来不应该奏效的信息
预览发送了两封信。 第一个用我们的HTML取代了虚拟键盘布局。 第二段告诉MathLive显示键盘,使其使新的布局:
MathLive承认了公共消息类型,但其顶级控制器使用了无源策略,这意味着它接受来自任何网络地址的信息。 它也不要求更新设置信息来自先前核准的窗口。 任何人如果能够联系到听众,并且知道消息格式,都可以取而代之。
当键盘打开后,MathLive将所提供的布局加入到一个HTML字符串中,并在真实的ChatGPT页面中被分配到内HTML. 一个叫CreativeHTML的帮助者最初看起来可能使数值消毒。 它没有;它标记字符串为可信任的,并且将我们的HTML通过没有改变。
这是令人兴奋的,但我们仍然没有 JavaScript 执行。 通过内置HTML插入的脚本标记不会自动运行。
下一步是使用 srcdoc 的 iframe,浏览器功能,让一个嵌入式框架直接在属性中携带其 HTML:
MathLive在没有沙盒属性的情况下将这个框架插入了主 ChatGPT 文档。 这给了我们两个有用的属性。 首先,srcdoc创建了外部脚本可以执行的新文档。 第二,由于主版ChatGPT页面创建了帧,浏览器给了它https://chatgpt.com的网页身份。 因此,新框架内的代码可以访问主页。
搭载YouTube JSONP的CSP
ChatGPT的内容安全政策,或CSP再次阻止了我们。 CSP控制一个页面可加载的脚本,而策略正确地被内置了JavaScript. 然而,它确实允许了YouTube的脚本。
YouTube的oEmbed端点支持JSONP风格的回调参数。 JSONP的响应是JavaScript而不是普通的JSON. 结束点试图拒绝无效的回调,但它在返回其验证错误之前将回调放入了回调:
反应是这样开始的:
结束我们的回电;由于YouTube已经被ChatGPT的CSP允许,浏览器将这个响应作为脚本加载,并在新框架内运行了我们的回调。
我们现在有任意的 JavaScript 作为 chatgpt.com 运行。 最初的"预览"仍然被隔离在其单独的网页地址上;我们通过说服主页中一个值得信赖的组件为我们创建一个新的,没有遮盖的文档而跨越了边界。
将实验室错误变成一个公共链接
迄今为止,这是一次有用的沙盒出逃,但第一个工作版本并不是令人信服的真实世界攻击。 它使用由研究人员控制的ChatGPT App作为发件人。 受害人需要先连接或援引App。
我们接下来尝试通过HTML的原生附件来运送有效载荷。 ChatGPT在同一沙盒预览中打开了附件,从而可以发送恶意的MathLive消息。
然而,当我们作为公开份额输出对话时,附件仍然存在,但先前的图表没有。 该图之所以重要,是因为它加载了"MathLive",并在主页上安装了脆弱的消息收听器。 没有它,附件仍然可以发送信息,但没有任何信息在听或处理。
突破是将两段直接放入一个普通的公开对话中:
- 主页上一个真实的图表初始化的数学。
- 后来的助理作者HTML预览发送了这两封信。
由此产生的/share/<id>链接为另一个用户保留了完整的链条。 攻击者通过电子邮件、聊天、支持票或社交贴纸,受害者不需要安装任何东西,加入攻击者的工作空间,或通过攻击者控制的流量进行认证。 他们只好参观攻击者共享的ChatGPT URL.
我们观察了两个互动版本。 在最稳定的再版中,被害者打开了股权并点击了一次ChatGPT的普通预览按钮。 我们后来将 HTML 块标记为可预览的块,以便 ChatGPT 自动打开。 在该版本中,打开链接是受害者的唯一点击。 我们把自动路径复制了三次,在一个单独的账户中,包括经过一次硬刷新之后,警报大约在4.5秒后出现。
图表和预览独立上载,因此有效载荷可以偶尔在MathLive准备好之前发送信息。 最终的航母通过在限定时间内重试同两条电文来处理该赛事。 主页的一位警卫确保了私人对话证明和外出请求只运行过一次。
此时,公用共享可以显示被浏览器识别出来自chatgpt.com的提示并可以在主页上添加一个横幅。 这证明了我们的代码运行的地方。 它还没有显示为什么虫子很重要。
警告还不够
此时,显示警示,是剥削所能做的最不有趣的事情。 JavaScript作为chatgpt.com运行,这意味着它可以使用与普通网络应用程序相同的认证上下文与ChatGPT交互。
有效载荷读取了该页面已经获得的会话信息,并用它来调用ChatGPT的对话API. 首先,它列举了受害者最近的对话。 然后,它可以请求为选定的私人对话提供消息数据:
获取/ 后端- api/ conversation?. & limit=20& order= updated&offset=0
获取/后端- api/ conversation/ < conversation-id>?num tuns=2
这是直接认证的 API 访问,而不是在屏幕上偶然可见的文本。 目标对话与恶意分享无关,从未与攻击者分享过。
一旦被注入的JavaScript得到响应,将数据发送出站是直截了当的。 有效载荷为我们控制的服务器创建了普通图像请求。
我们故意只把一个小部分的响应发到场外。 注入的代码已经收到经认证的谈话回复,并且可以读取其内容,因此脆弱性不仅限于这一价值。
结果形成了实用的跨用户数据-被窃取的原始。 一个恶意的公开对话可以在另一个用户的签名-in ChatGPT会话内执行,列举私人对话,检索私人消息内容,并将所选数据发送到攻击者控制的服务器。
因为代码运行与网络应用程序的认证访问,它的覆盖范围本质上不局限于我们的证据中使用的对话端点。 通过ChatGPT认证的前端API所暴露出的其他数据和动作坐落在同一安全边界内。 基本上我们可以代表被害人的ChatGPT账户做任何行动
出了什么问题 OpenAI修好了
预览沙盒本身就起作用了。 预览中的代码无法直接读取 ChatGPT 页面。 问题在于主页的另一部分愿意接受其指示。
三个设计决定将这个错误变成了一个充分利用:
- MathLive接受了一个改变状态的信息但没有证明是哪个框架发出的
- 消息格式允许以键盘放出数据提供原始 HTML。
- HTML可以创建一个无法保存的文档,它继承了chatgpt.com的身份。
YouTube的回应是CSP的最后工作,仅屏蔽该端点会移除一条路径到JavaScript,同时仍然留下一个无法被信任的预览可以将HTML插入到ChatGPT.
OpenAI同时复制了JavaScript执行和私人对话证明。 平面被推出两个版本的ChatGPT网络前端。 在推出期间,CSP的更改屏蔽了部分原链条,而一个版本仍需要更新。 10月26日,OpenAI确认,现在两个版本都拒绝了报告使用的跨框架的MathLive信息,并标出问题已解决。
防御性课程很简单:
- 信息接收器应核查发送者的网页地址和获得许可的确切窗口。
- 跨窗口设置不应包含原始 HTML。
- 动态生成的文档在内容受到不可信框架影响时,仍应被放入沙箱。
- CSP应该是备用控制,而不是攻击者控制的HTML到达主站点后的唯一屏障。
这个bug很有趣,因为每个单个特性看起来都是合理的。 预览被隔离。 图形可以装入数学键盘。 框架可以交换消息。 CSP允许一个主要的媒体供应商。 可以分享公共对话。 只有将所有这些特点合并成一页,脆弱性才会出现。
时间线
技术指出
上面的主要故事包含了完整的开发链。 读者详细情况如下:
本文由 AI宝库自动抓取并翻译,版权归原作者所有;如需查看原文请点下方链接。
Do the Math: How We Took Over ChatGPT Accounts
By bypassing ChatGPT's sandbox and content security policy, our research team was able to take over anyone's account simply by sending them a link.
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.
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.
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.
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.
The chain started with two features that looked harmless on their own: a graph and an HTML Preview.
Technical walkthrough
It started with a graph and a Preview
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.
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.
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.
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.
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.
The message that should not have worked
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:
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.
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.
This was exciting, but we still did not have JavaScript execution. Script tags inserted through innerHTML do not automatically run.
The next step was an iframe using srcdoc, a browser feature that lets an embedded frame carry its HTML directly in an attribute:
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.
Bypassing CSP with YouTube JSONP
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.
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:
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.
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.
Turning a lab bug into one public link
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.
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.
However, when we exported the conversation as a public share, the attachment remained but the earlier graph did not. The graph was essential because it loaded MathLive and installed the vulnerable message listener in the main page. Without it, the attachment could still send the messages, but nothing was listening to or processing them.
The breakthrough was putting both pieces directly into one ordinary public conversation:
- A genuine graph initialized MathLive in the main page.
- A later assistant-authored HTML Preview sent the two messages.
The resulting /share/<id> link preserved the complete chain for another user. The attacker could deliver it the same way they would send any shared ChatGPT conversation—through email, chat, a support ticket, or a social post. The victim did not need to install anything, join the attacker's workspace, or authenticate through an attacker-controlled flow. They just had to visit the attacker's shared ChatGPT URL.
We observed two versions of the interaction. In the most stable reproduction, the victim opened the share and clicked ChatGPT's normal Preview button once. We later marked the HTML block as previewable so that ChatGPT opened it automatically. In that version, opening the link was the victim's only click. We reproduced the automatic path three times in a separate account, including after a hard refresh, with the alert appearing after roughly 4.5 seconds.
The graph and Preview loaded independently, so the payload could occasionally send its message before MathLive was ready. The final carrier handled that race by retrying the same two messages for a limited period. A guard in the main page ensured that the private-conversation proof and outbound request ran only once.
At this point, the public share could display an alert identified by the browser as coming from chatgpt.com and could add a banner to the main page. That proved where our code was running. It did not yet show why the bug mattered.
An alert was not enough
At this point, displaying an alert was the least interesting thing the exploit could do. The JavaScript was running as chatgpt.com, which meant it could interact with ChatGPT using the same authenticated context as the normal web application.
The payload read the session information already available to the page and used it to call ChatGPT's conversation APIs. First, it enumerated the victim's recent conversations. It could then request message data for a selected private conversation:
GET /backend-api/conversations?...&limit=20&order=updated&offset=0
GET /backend-api/conversations/<conversation-id>?num_turns=2
This was direct authenticated API access, not scraping text that happened to be visible on screen. The target conversation was unrelated to the malicious share and had never been shared with the attacker.
Once the response was available to the injected JavaScript, sending data off-site was straightforward. The payload created an ordinary image request to a server we controlled.
We deliberately sent only a small piece of the response off-site. The injected code had already received the authenticated conversation response and could read its contents, so the vulnerability was not limited to that value.
The result was a practical cross-user data-theft primitive. A malicious public conversation could execute inside another user's signed-in ChatGPT session, enumerate private conversations, retrieve private message content, and send selected data to an attacker-controlled server.
Because the code ran with the web application's authenticated access, its reach was not inherently limited to the conversation endpoints used in our proof. Other data and actions exposed through ChatGPT's authenticated frontend APIs sat inside the same security boundary. Essentially, we could perform any action on behalf of the victim's ChatGPT account.
What went wrong and what OpenAI fixed
The Preview sandbox itself worked. Code inside Preview could not directly read the ChatGPT page. The problem was that another component in the main page was willing to accept its instructions.
Three design decisions turned that mistake into a full exploit:
- MathLive accepted a state-changing message without proving which frame sent it.
- The message format allowed raw HTML to be supplied as keyboard-layout data.
- That HTML could create an unsandboxed document that inherited the chatgpt.com identity.
The YouTube response was the final workaround for CSP, not the root cause. Blocking only that endpoint would have removed one path to JavaScript while still leaving an untrusted Preview able to insert HTML into ChatGPT.
OpenAI reproduced both the JavaScript execution and the private-conversation proof. The fix was rolled out across two versions of ChatGPT's web frontend. During the rollout, CSP changes blocked part of the original chain while one version still needed an update. On October 26, OpenAI confirmed that both versions now rejected the cross-frame MathLive messages used by the report and marked the issue resolved.
The defensive lessons are fairly simple:
- A message receiver should verify both the sender's web address and the exact window that was given permission.
- Cross-window settings should not contain raw HTML.
- Dynamically created documents should remain sandboxed when their content can be influenced by an untrusted frame.
- CSP should be a backup control, not the only barrier after attacker-controlled HTML reaches the main site.
This bug was interesting because every individual feature looked reasonable. Preview was isolated. Graphs could load a math keyboard. Frames could exchange messages. CSP allowed a major media provider. Public conversations could be shared. The vulnerability appeared only when all of those features were combined in one page.
Timeline
Technical notes
The main story above contains the complete exploit chain. The details below are included for readers w
Auto-fetched and translated by AI Gem Box. All rights belong to the original author.