We Took over ChatGPT Accounts
TL;DR · AI 摘要
Do the Math: How We Took Over ChatGPT Accounts - Parameter Labs Close Do the Math: How We Took Over ChatGPT Accounts By...
核心要点
- 主题聚焦:We Took over ChatGPT Accounts
- 来源:Hacker News Best,建议结合原文判断细节。
- AI 分析暂不可用,本条为保底评分与摘要。
Do the Math: How We Took Over ChatGPT Accounts - Parameter
Labs
Close
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.
Mukul Goyal
Oct 28, 2026
12 mins
October 26, 2026
Fixed
P1
Severity
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.
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"]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 没有对我们在意的设置执行该检查。其监听器接受了一条消息,该消息更改了虚拟键盘布局,且该布局被允许包含 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 在没有 sandbox 属性的情况下将此框架插入到主 ChatGPT 文档中。这给了我们两个有用的属性。首先,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。原始预览仍然隔离在其独立的网页地址上;我们通过说服主页面中的可信组件为我们创建了一个未沙盒化的文档,从而跨越了边界。
将实验室漏洞转化为一个公开链接
到目前为止,这只是一个有用的沙盒逃逸方法,但第一个可用版本并不能算是令人信服的真实攻击场景。它使用了研究人员控制的ChatGPT应用作为发送方,受害者需要先连接或调用该应用。
我们接下来尝试通过原生HTML附件传递有效载荷。ChatGPT会在同一个沙盒预览窗口中打开附件,从而能够发送恶意MathLive消息。
然而,当我们将对话导出为公开分享时,附件仍然存在但之前的图表却消失了。这个图表至关重要,因为它加载了MathLive并在主页面安装了易受攻击的消息监听器。没有它,附件仍然可以发送消息,但没有任何东西在监听或处理这些消息。
突破点在于将这两个组件直接放入一个普通的公开对话中:
- 一个真实的图表在主页面初始化了MathLive。
- 后续由助手生成的HTML预览发送了两条消息。
生成的/share/<id>链接为其他用户保留了完整的链式结构。攻击者可以像发送任何共享的ChatGPT对话一样传递这个链接——通过电子邮件、聊天、支持工单或社交媒体帖子。受害者不需要安装任何东西、加入攻击者的 workspace 或通过攻击者控制的流程进行身份验证。他们只需访问攻击者共享的ChatGPT URL即可。
我们观察到两种交互方式。在最稳定的复现场景中,受害者打开分享链接并点击一次ChatGPT的正常预览按钮。我们后来将HTML块标记为可预览,使ChatGPT自动打开它。在这种版本中,打开链接是受害者唯一的点击操作。我们在另一个账户上三次复现了自动路径,包括硬刷新后,警报在约4.5秒后出现。
图表和预览是独立加载的,因此有效载荷有时会在MathLive准备就绪之前发送消息。最终的载体通过在有限时间内重试相同的两条消息处理了这种竞争条件。主页面中的一个防护机制确保私密对话证明和出站请求仅执行一次。
公共分享修改了真实的ChatGPT页面
此时,公共分享可以显示浏览器识别为来自chatgpt.com的警报,并可以向主页面添加横幅。这证明了我们的代码正在运行的位置。但此时还无法说明这个漏洞为何重要。
仅显示警报是不够的
此时,显示警报是这个攻击能做的最不值得关注的事情。JavaScript以chatgpt.com的身份运行,这意味着它可以使用与正常网页应用相同的认证上下文与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执行路径,但仍然允许不受信任的预览向ChatGPT插入HTML。
OpenAI复现了JavaScript执行和私人对话证明。修复措施部署在ChatGPT网页前端的两个版本中。在部署过程中,CSP变更阻断了原始链的一部分,而其中一个版本仍需要更新。2026年10月26日,OpenAI确认两个版本现已拒绝报告中使用的跨框架MathLive消息,并将问题标记为已解决。
防御性教训相当简单:
- 消息接收方应验证发送方的网络地址和被授权的具体窗口。
- 跨窗口设置不应包含原始HTML。
- 当内容可能受到不受信任框架影响时,动态创建的文档应保持沙箱化。
- CSP应作为后备控制,而非攻击者控制的HTML到达主站后的唯一屏障。
这个漏洞之所以有趣,是因为每个单独功能看起来都合理。预览是隔离的。图表可以加载数学键盘。框架可以交换消息。CSP允许主要媒体提供商。公共对话可以共享。只有当所有这些功能在同一页中组合时,漏洞才会出现。
时间线
日期 | 事件 --- | --- 2026年10月13日 | 确认MathLive消息到HTML路径与ChatGPT生产代码的关联 2026年10月14日 | 通过正常UI复现完整链路,并将修复警报升级为任意JavaScript执行 2026年10月16日 | 确认通过一次预览点击即可在不同账户和工作空间之间实现公共分享传输 2026年10月17日 | 确认通过分享导航自动触发预览,并在强制刷新后复现该现象 2026年10月18日 | 向OpenAI漏洞赏金计划提交报告 2026年10月19日 | 添加受控私聊信息泄露证明 2026年10月21日 |
OpenAI复现了该问题;严重程度从最初的P2分级提升至P1。
10月22日
OpenAI报告称一个Web前端已修复,而第二个前端仍在更新中。
10月23日
CSP变更阻断了部分流程,一个前端更新仍处于进行中。
10月26日
OpenAI确认两个前端均拒绝了报告的消息,成功解决问题,并发放总计5000美元的赏金。
技术说明
上述主流程包含了完整的攻击链。以下细节为希望更深入了解生产代码和载荷转换过程的读者提供参考。
相关反混淆后的控制流等价于:
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重写了回调中的角度括号,因此载荷使用经典函数语法而非箭头函数。以;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 <受害者令牌>
ChatGPT-Account-ID: <受害者账户>
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仅当 detail 请求返回 403、404 或 405 状态码时,代码才会回退到旧版路由 /backend-api/conversation/<id>?include_full_conversation=true。验证过程仅从响应中提取了少量选定值。在发起外部图像请求前,系统会清除与 token、account、bootstrap、session 和请求头相关的引用。
关键启示
- 隔离强度取决于所有跨越它的桥梁。即使是一个沙箱化框架,若特权监听器错误地信任了来源不明的消息,仍可能危及父级上下文。
- postMessage 接收方在接受状态变更指令前,必须同时验证 event.origin 和 event.source 的来源合法性。
- 通过未沙箱化的全新文档和允许的第三方脚本端点,HTML 注入可能演变为 JavaScript 执行。CSP 是有价值的纵深防御手段,但无法将不安全的渲染路径转化为安全状态。
- 同源 JavaScript 的影响必须在受害者已认证的上下文中评估。一旦代码在 chatgpt.com 域名下运行,即可访问与正常前端相同的私有数据和账户操作权限,而不仅仅是弹出警告框。
- 跨功能测试至关重要。该漏洞链存在于公共分享、预览、MathLive、框架创建和 CSP 等功能的边界缝隙中,而非单一功能模块内部。
更深层的教训是:安全边界必须覆盖整个产品全链路,而不仅仅是单个组件内部。所有公开或可共享的接口都应视为攻击者控制的输入,跨框架消息必须严格验证来源,不可信的标记内容在整个渲染路径中都应保持沙箱化隔离。
next
四小时发现四个漏洞:一句代码引发的连锁反应
back to all posts /