Parameter 安全研究:公开 ChatGPT 分享链接可在受害者登录会话中执行任意代码
We Took over ChatGPT Accounts
Parameter 团队披露一种针对 ChatGPT 的 XSS 攻击链:攻击者发布含交互图表和 HTML Preview 的公开分享对话,借助 MathLive 键盘布局消息未验证来源、innerHTML 注入无沙箱 iframe、YouTube oEmbed JSONP 绕过 CSP,使 JavaScript 以 chatgpt.com 身份在受害者登录会话中运行。
作者第一手复现完整攻击链并给出时间线与修复过程,读者可以借此理解沙箱隔离为何会在跨组件组合下失效。
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"]技术详解
起点:一个图表和一个预览
这项研究最初的问题很简单:如果预览代码被关在另一个网站里,它还能和谁通信?
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,并在主页面中安装存在漏洞的消息监听器。没有它,附件仍然可以发送消息,但没有任何东西在监听或处理这些消息。
突破点在于把两部分直接放进一个普通的公开对话里:
- 一个真实的图表在主页面中初始化了 MathLive。
- 随后由助手编写的 HTML Preview 发送了那两条消息。
最终得到的 /share/<id> 链接为其他用户保留了完整的攻击链。攻击者可以像发送任何共享的 ChatGPT 对话一样投递它——通过电子邮件、聊天、支持工单或社交帖子。受害者无需安装任何东西,无需加入攻击者的工作区,也无需通过攻击者控制的流程进行身份验证。他们只需要访问攻击者分享的 ChatGPT URL。
我们观察到了两种交互版本。在最稳定的复现中,受害者打开分享链接并点击了一次 ChatGPT 正常的预览按钮。后来我们把 HTML 块标记为可预览,让 ChatGPT 自动打开它。在那个版本里,打开链接是受害者唯一的一次点击。我们在另一个账号中复现了自动路径三次,包括强制刷新之后,警报大约在 4.5 秒后出现。
图表和预览是独立加载的,所以载荷有时会在 MathLive 就绪之前就发出消息。最终的载体通过在一段有限时间内重试同样的两条消息来处理这个竞态。主页面中的一个防护机制确保私密对话证明和对外请求只执行一次。

此时,公开分享已经能显示一个被浏览器识别为来自 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 可用,把数据发到站外就很简单了。载荷向我们控制的服务器创建了一个普通的图片请求。
我们故意只把响应的一小部分发到站外。注入的代码已经收到了已认证的对话响应,能读取其内容,所以这个漏洞并不局限于那个值。

结果是一个实用的跨用户数据窃取原语。一个恶意的公开对话可以在另一个用户已登录的 ChatGPT 会话中执行,枚举私密对话,获取私密消息内容,并把选定的数据发送到攻击者控制的服务器。
由于代码是以 Web 应用的已认证访问权限运行的,它的影响范围本质上并不局限于我们证明中使用的对话端点。通过 ChatGPT 已认证前端 API 暴露的其他数据和操作都处在同一个安全边界内。基本上,我们可以代表受害者的 ChatGPT 账号执行任何操作。
出了什么问题,OpenAI 修了什么
预览沙箱本身是有效的。预览中的代码无法直接读取 ChatGPT 页面。问题在于主页面中的另一个组件愿意接受它的指令。
三个设计决策把这个错误变成了完整的漏洞利用:
- MathLive 接受了一条会改变状态的消息,却没有证明是哪个框架发送的。
- 消息格式允许把原始 HTML 作为键盘布局数据提供。
- 那段 HTML 可以创建一个未沙箱化的文档,继承
chatgpt.com的身份。
YouTube 那个接口只是绕开 CSP 的最后一步,并不是漏洞的根源。只封掉这个接口,等于堵住了一条通往 JavaScript 的路径,但不受信任的 Preview 仍然能把 HTML 注入 ChatGPT。
OpenAI 复现了 JavaScript 执行和私密对话泄露这两点。修复分两个版本在 ChatGPT 网页前端上线。推送期间,CSP 的改动拦住了原攻击链的一部分,但还有一个版本需要更新。10 月 26 日,OpenAI 确认两个版本都已拒绝报告中使用的跨框架 MathLive 消息,并将该问题标记为已解决。
防御上的经验其实很简单:
- 消息接收方既要校验发送方的网址,也要校验被授权的具体窗口。
- 跨窗口的设置不应包含原始 HTML。
- 动态创建的文档,如果内容可能受不受信任的框架影响,就应保持沙箱隔离。
- CSP 应当作为兜底控制,而不是攻击者可控的 HTML 进入主站之后的唯一屏障。
这个漏洞有意思的地方在于,每个单独的功能看起来都合理。Preview 是隔离的;图表可以加载数学键盘;框架之间可以交换消息;CSP 允许了一家大型媒体服务商;公开对话可以分享。只有当这些功能全部出现在同一个页面上时,漏洞才显现出来。
时间线
| 日期 | 事件 |
|---|---|
| 2026 年 10 月 13 日 | 在 ChatGPT 生产代码上确认了 MathLive 消息转 HTML 的路径。 |
| 10 月 14 日 | 通过正常界面复现了整条攻击链,并把固定的弹窗升级为任意 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 确认两个前端都已拒绝报告中的消息,问题已解决,并发放了总计 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 在回调中改写了尖括号,因此载荷使用了经典的 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:AI 热帖 · parameter.ai