跳到正文
Hacker News· armcat·· 6 小时前精选AI 评分80

Parameter 团队发现 ChatGPT 公开分享链接可接管账户,OpenAI 已修复并支付 5000 美元赏金

We Took over ChatGPT Accounts

AI 导读

Parameter 团队在 2026 年 10 月发现,攻击者只需发布一个普通 ChatGPT 公开分享链接,受害者打开后其中的 JavaScript 就会以 chatgpt.com 身份在其已登录会话中执行,可枚举会话列表、读取从未分享的私密对话并外传数据。

推荐理由

完整披露一条从公开分享链接到账户接管的利用链,并给出可迁移的跨窗口消息校验教训。

正文 · AI 翻译

2026 年 10 月,我们发现了一种方法,可以将一个普通的公开 ChatGPT 分享链接变成由攻击者控制的 JavaScript,并在打开该链接的已登录用户的会话中运行。

攻击者可以创建一个恶意对话,使用 ChatGPT 的常规分享功能将其发布,并将链接发送给另一名用户。当受害者打开该分享时,载荷会以 chatgpt.com 的身份执行,并能够访问受害者的已认证 Web 会话。

我们确认,这段代码可以枚举受害者的对话列表,从一段从未与攻击者分享过的私密对话中获取内容,并将该对话中的数据发送到外部服务器。该载荷并不局限于修改页面或弹出提示——它可以以受害者的身份直接调用 ChatGPT 的已认证 API,实质上使我们能够接管受害者的 ChatGPT 账户。

受害者无需安装 App、加入 Project、粘贴代码或打开开发者工具。打开分享链接是唯一需要执行的操作。

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 的 Web 身份。因此,新框架内的代码可以访问父页面。

利用 YouTube JSONP 绕过 CSP

ChatGPT 的内容安全策略(CSP)再次阻止了我们。CSP 控制页面可以加载哪些脚本,该策略正确地阻止了内联 JavaScript。不过,它允许来自 YouTube 的脚本。

YouTube 的 oEmbed 端点支持 JSONP 风格的 callback 参数。JSONP 响应是 JavaScript 而非普通 JSON。该端点试图拒绝无效的 callback,但它在返回验证错误之前就将 callback 放入了响应中:

https://www.youtube.com/oembed?callback=globalThis.__marker=31337;void

响应开头如下:

// API callback
globalThis.__marker=31337;void({
  "error": {
    "code": 400,
    "message": "Invalid JSONP callback name: ..."
  }
});

让我们的 callback 以 ;void 结尾,使 YouTube 追加的对象变得无害。由于 ChatGPT 的 CSP 已经允许 YouTube,浏览器将此响应作为脚本加载,并在新框架内运行了我们的 callback。

现在我们拥有了以 chatgpt.com 身份运行的任意 JavaScript。原始的 Preview 仍然隔离在其独立的网址上;我们通过诱使主页面中一个受信任的组件为我们创建一个新的、未沙箱化的文档,从而跨越了边界。

将一个实验室漏洞变成一条公开链接

到目前为止,这是一个有用的沙箱逃逸,但第一个可工作的版本并不是一个有说服力的真实世界攻击。它使用了一个由研究人员控制的 ChatGPT App 作为发送方。受害者需要先连接或调用该 App。

我们接下来尝试通过原生 HTML 附件传递载荷。ChatGPT 在同一个沙箱化的 Preview 中打开该附件,因此它可以发送恶意的 MathLive 消息。

然而,当我们将对话导出为公开分享时,附件保留了,但之前的图表没有保留。该图表至关重要,因为它加载了 MathLive 并在主页面中安装了存在漏洞的消息监听器。没有它,附件仍然可以发送消息,但没有任何东西在监听或处理它们。

突破点在于将两部分直接放入一个普通的公开对话中:

  1. 一个真实的图表在主页面中初始化了 MathLive。
  2. 稍后由助手编写的 HTML Preview 发送了这两条消息。

生成的 /share/<id> 链接为另一个用户保留了完整的攻击链。攻击者可以像发送任何共享的 ChatGPT 对话一样传递它——通过电子邮件、聊天、支持工单或社交帖子。受害者无需安装任何东西、加入攻击者的工作区,或通过攻击者控制的流程进行身份验证。他们只需访问攻击者共享的 ChatGPT URL。

我们观察到了该交互的两种版本。在最稳定的复现中,受害者打开分享链接并点击一次 ChatGPT 正常的 Preview 按钮。后来我们将该 HTML 块标记为可预览,以便 ChatGPT 自动打开它。在该版本中,打开链接是受害者唯一的点击操作。我们在另一个账户中三次复现了自动路径,包括在强制刷新之后,警报大约在 4.5 秒后出现。

图表和 Preview 独立加载,因此载荷有时可能在 MathLive 就绪之前就发送消息。最终的载体通过在一段有限时间内重试相同的两条消息来处理该竞态。主页面中的一个防护机制确保私有对话证明和出站请求只运行一次。

The public share modified the real ChatGPT page
公开分享修改了真实的 ChatGPT 页面

此时,公开分享可以显示一个被浏览器识别为来自 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 可用,将数据发送到站外就很简单了。载荷向我们控制的服务器创建了一个普通的图片请求。

我们故意只将响应的一小部分发送到站外。注入的代码已经收到了已认证的对话响应,并且可以读取其内容,因此该漏洞并不局限于该值。

Controlled proof that private conversation data could leave the victim's session
受控证明:私有对话数据可以离开受害者的会话

结果是一个实用的跨用户数据窃取原语。一个恶意的公开对话可以在另一个用户已登录的 ChatGPT 会话中执行,枚举私有对话,检索私有消息内容,并将选定数据发送到攻击者控制的服务器。

由于代码以 Web 应用的已认证访问权限运行,其影响范围本质上并不局限于我们证明中使用的对话端点。通过 ChatGPT 已认证前端 API 暴露的其他数据和操作也处于同一安全边界内。本质上,我们可以代表受害者的 ChatGPT 账户执行任何操作。

出了什么问题以及 OpenAI 修复了什么

Preview 沙箱本身工作正常。Preview 内的代码无法直接读取 ChatGPT 页面。问题在于主页面中的另一个组件愿意接受其指令。

三个设计决策将这一错误变成了完整的漏洞利用:

  1. MathLive 接受了改变状态的消息,却没有证明是哪个框架发送的。
  2. 消息格式允许将原始 HTML 作为键盘布局数据提供。
  3. 该 HTML 可以创建一个未沙箱化的文档,并继承 chatgpt.com 身份。

YouTube 响应是 CSP 的最终变通方案,而非根本原因。仅封锁该端点只会移除一条通往 JavaScript 的路径,而仍会留下一个不受信任的 Preview,使其能够向 ChatGPT 中插入 HTML。

OpenAI 复现了 JavaScript 执行和私密对话证明。修复已分两个版本的 ChatGPT 网页前端推出。在推出期间,CSP 变更阻断了原始链条的一部分,而其中一个版本仍需更新。10 月 26 日,OpenAI 确认两个版本现在都会拒绝报告中使用的跨框架 MathLive 消息,并将该问题标记为已解决。

防御方面的教训相当简单:

  • 消息接收方应同时验证发送方的网址和获得许可的确切窗口。
  • 跨窗口设置不应包含原始 HTML。
  • 当动态创建的文档内容可能受到不受信任框架影响时,应使其保持沙箱化。
  • CSP 应作为后备控制,而不是攻击者控制的 HTML 到达主站后的唯一屏障。

这个漏洞之所以有趣,是因为每个单独的功能看起来都合理。Preview 是隔离的。图表可以加载数学键盘。框架可以交换消息。CSP 允许了一家大型媒体提供商。公开对话可以分享。只有当所有这些功能组合在一个页面中时,漏洞才会出现。

时间线

日期事件
2026 年 10 月 13 日针对 ChatGPT 的生产代码确认了 MathLive 消息到 HTML 的路径。
10 月 14 日通过正常 UI 复现了该链条,并将固定警报升级为任意 JavaScript。
10 月 16 日确认了不同账户和工作区之间通过一次 Preview 点击实现公开分享投递。
10 月 17 日确认了从分享导航自动触发 Preview,并在硬刷新后复现。
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,该 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