ARTICLE / ai
一条ChatGPT分享链接接管账号:Preview沙箱是如何被自己人打穿的
一条 ChatGPT 分享链接接管账号:Preview 沙箱是如何被自己人打穿的
安全研究团队 Parameter AI 在 10 月 26 日公开了一篇披露报告:攻击者只需要发布一条普通的 ChatGPT 对话、用官方分享功能生成链接发给别人,受害者点开这个链接,攻击者的 JavaScript 就会以受害者已登录的身份在 chatgpt.com 里运行。报告确认这段代码能列出受害者的会话列表、读取从未分享过的私聊内容,并把选中的数据发到外部服务器。OpenAI 在 10 月 26 日修复,定级 P1,支付合计 5,000 美元赏金。
这条链路里没有哪个环节是"带毒"的:Preview 有沙箱、图形组件加载的是正规数学库、CSP 配置看起来也合理、分享是对话产品的默认能力。问题出在把它们拼到同一个页面之后。对做 Web 安全和 AI 产品的人来说,这个案例的价值恰恰在于每一步都"看着没问题"。
链条的第一步:一个没人查来信人的监听器
ChatGPT 的 Preview 功能可以把对话里的 HTML 代码块变成一个小网页。因为里面可能包含任意代码,ChatGPT 把它放在另一个域名下的受限 iframe 里运行——Preview 里的 JavaScript 跑着,但浏览器把它当成"别的网站",读不到外面已登录的主页面。这一层隔离本身是按设计工作的,报告也承认 Preview 里的代码直接读 parent.document 是失败的。
真正的裂缝在主页面这边。对话里如果有一个交互式图形,背后会加载数学渲染库 MathLive。加载 MathLive 时发生了一件与画图无关的事:它在主 ChatGPT 页面注册了一个消息监听器,处理 MathLive 软键盘相关的 postMessage。
postMessage 是跨帧通信的合法通道——两个互相不能读对方页面的 frame 可以互发消息。它安全的前提是收信方检查"这封信是谁寄的"。MathLive 的这个监听器没有做这个检查:它接受一条修改虚拟键盘布局的消息,而布局字段允许携带原始 HTML。
于是两条本不相干的能力接上了:图形负责在主页面安装收信人,Preview 负责当那个不受信的寄信人。报告给出的反混淆后控制流等价于:监听器只判断消息类型是不是 mathlive#virtual-keyboard-message,然后走一个 origin 策略为 none 的校验——等于任何网页地址发来的都收;也不要求发送方是经过批准的窗口。
Preview 连发两条消息:第一条把键盘布局换成攻击者的 HTML,第二条让 MathLive 显示键盘。MathLive 把布局字符串拼成 HTML,赋给主页面真实 DOM 的 innerHTML。这里还有一个细节值得记:负责包装这段 HTML 的辅助函数 createHTML 乍看像消毒器,实际只是 Trusted Types 的恒等包装——它把字符串标记为"可信"原样放行。Trusted Types 能限制谁有权往 HTML 汇点赋值,但恒等策略不会让攻击者控制的字符串变安全。
从 HTML 注入到 JS 执行:两个跳板
innerHTML 插进去的 <script> 标签不会自动执行,这是浏览器的基本规则。攻击者用的是 <iframe srcdoc>:srcdoc 让内嵌帧直接在属性里携带自己的 HTML,触发一次全新的文档解析。更关键的是,这个 iframe 是主 ChatGPT 页面创建的,且没有 sandbox 属性——浏览器因此给了它 https://chatgpt.com 的源身份。新文档里的外部脚本可以执行,而且它能访问父页面。
还差最后一道门:CSP。主页面和继承下来的 CSP 禁止内联脚本,但 script-src-elem 允许 www.youtube.com。YouTube 的 oEmbed 端点支持 JSONP 风格的 callback 参数,而它对非法 callback 名的处理方式暴露了顺序错误:它先把 callback 拼进响应体,再返回校验错误。响应以 text/javascript 加 nosniff 返回 200,开头形如:
callback 尾部用 ;void 把 YouTube 追加的错误对象变成无害表达式,载荷就借着一台被 CSP 信任的服务器跑了起来。报告还记录了一个工程细节:Google 会重写 callback 里的尖括号,所以载荷只能用经典 function 语法而非箭头函数。最终私有会话证明版本的回调 5,533 字符,拼出的完整 iframe 标记 7,390 字符。
到这里,代码已经以 chatgpt.com 的身份运行,而最初的 Preview 仍然待在自己的隔离域里——越界是靠说服主页面里一个受信组件为攻击者创建了一个未沙箱化的新文档完成的。
从实验室漏洞到一条公开链接
第一版可用 PoC 需要一个研究者控制的 ChatGPT App 当寄信人,受害者得先连接或调用那个 App,实战性不够。团队试过用原生 HTML 附件投递载荷:附件确实在同一个沙箱 Preview 里打开、能发出恶意消息,但把对话导出为公开分享链接后,附件保留了、前面的图形丢了——没有图形就没有 MathLive,没有 MathLive 就没有人在主页面听信这些消息。
最终的载体是把两块都放进同一条普通对话:一个真实图形先在主页面初始化 MathLive,后面一条助手生成的 HTML Preview 负责发消息。这样产出的 /share/<id> 链接完整保留整条链,攻击者像分享任何对话一样把它发到邮件、聊天或社交帖子里。受害者不需要安装任何东西、不需要加入工作区、不需要走任何认证流程,只需打开链接。
复现行为有两种形态:最稳定的版本需要受害者点开分享后正常点击一次 Preview 按钮;后来团队把 HTML 块标记为可预览,ChatGPT 会自动打开它,点开链接成为唯一的交互。自动路径在独立账号上复现了三次(含硬刷新),alert 大约在 4.5 秒后弹出。图形和 Preview 各自独立加载,存在载荷抢跑于 MathLive 就绪的竞态,载体靠有限次重试两条消息来兜底。
为什么 alert 只是起点
代码运行在受害者的已登录上下文里,意味着它能做前端应用能做的读操作。载荷从主页面的 #client-bootstrap 解析出会话 access token 和账号 ID(并要求 token 内的账号与会话一致),带上 Authorization: Bearer <token> 和 ChatGPT-Account-ID 头直接调已认证 API:
这不是抓取屏幕上恰好可见的文字,而是直接的已认证接口访问。目标会话与那条恶意分享无关、从未分享给攻击者。拿到响应后外传数据很朴素:构造一个指向攻击者服务器的图片请求即可。报告刻意只外传了响应里的一小段选定值——注入代码已经收到并可读整个响应,漏洞的影响面不取决于那一个值。
由于代码享有 Web 应用的认证边界,它的可达范围天然不限于这两个会话接口:前端认证 API 暴露的其他数据和操作都在同一条线内。这就是"接管账号"这个说法的依据。
OpenAI 修了什么,以及修复节奏里的隐患
报告总结把根因归为三个设计决定:状态变更消息未验证发送方;跨窗口设置允许携带原始 HTML;动态创建的文档在内容受不受信帧影响时仍保持未沙箱化。YouTube JSONP 只是绕过 CSP 的最后 workaround,不是根因——只封这一个端点的话,不受信 Preview 依然能往主页面注 HTML。
修复分两批覆盖 ChatGPT Web 的两套前端。过渡期的状态值得注意:10 月 23 日 CSP 变更拦住了链条的一部分,但另一套前端仍未更新;直到 10 月 26 日两套前端都拒收报告中的跨帧 MathLive 消息,问题才算关闭。多套前端灰度并行时,“部分缓解"和"已修复"之间的窗口期对防御方是真实的暴露时间。
完整时间线(来自披露报告):
| 日期 | 事件 |
|---|---|
| 10 月 13 日 | 在生产代码上确认 MathLive 消息到 HTML 的路径 |
| 10 月 14 日 | 通过正常 UI 复现全链,固定 alert 升级为任意 JS |
| 10 月 16 日 | 跨账号跨工作区确认公开分享投递,单次 Preview 点击生效 |
| 10 月 18 日 | 提交给 OpenAI bug bounty |
| 10 月 21 日 | OpenAI 复现,严重级从初评 P2 升到 P1 |
| 10 月 26 日 | 两套前端确认修复,合计 $5,000 赏金 |
给防御方的四条落点
把本案翻译成可执行的检查清单,每条都能落到具体代码位置:
- 所有
postMessage接收器同时验event.origin和event.source。只看消息 type 不看寄件人,等于邮箱按标题分拣。审计第三方库注入全局监听器的场景——本案的监听器不是 ChatGPT 自己写的,是它加载的组件装的。 - 跨窗口传递的配置里不允许出现 HTML 载荷。键盘布局这种字段本来没有任何理由接受 markup。
- 凡内容可能被不受信输入影响的动态文档,创建时就挂 sandbox。
srcdociframe 缺 sandbox 属性是本案从注 HTML 走到拿同源身份的关键一跳。 - CSP 是纵深防御的备份层,不是 HTML 到达主站之后的唯一屏障;同时警惕 CSP 白名单里的 JSONP 端点——允许一个大媒体域名加载脚本,就要预期该域名的响应体会被执行。
还有一条产品层的推论:任何公开或可分享的界面都应视为攻击者可控输入。分享链接是对话产品的门面功能,但它把"攻击者完全控制的对话内容"送进了"受害者已认证的会话环境”。做 AI 产品时值得专门问一句:我的分享面、预览面、嵌入面里,哪一块的渲染路径穿过了主站 DOM?
检测视角:这类攻击在你的日志里长什么样
站在蓝队侧,这条链有两个可观测特征。其一是行为面:账号在无用户交互的情况下突发枚举会话列表的 API 序列(/backend-api/conversations 连续分页拉取紧跟单个会话详情读取),随后伴随一个向陌生域的图片 GET 请求——这是典型的"读取+盲额外带"组合。如果你的网关能看到终端侧流量,对聊天类 SaaS 域名后的异常 referer 空值图片请求值得留告警。其二是内容面:公开分享页中出现"图形 + HTML Preview"的组合本身就是该链的必要条件之一,平台侧对分享内容做静态特征筛查(srcdoc、oEmbed callback 参数、超长 URL 编码载荷)能在服务端拦下大部分自动化投递。
对普通用户,现阶段唯一有效的自保动作是降低"随手点开分享链接"的默认信任:来源不明的 chatgpt.com/share 链接,哪怕标题看起来是你关心的话题,也不要在已登录浏览器里直接打开。这不优雅,但在"每个组件单看都合理"的复合漏洞面前,人的怀疑心仍然是最后一道没被 MathLive 装过监听的防线。
本文面向授权安全研究与防御建设,未经授权对任何系统实施文中技术属违法行为。文中漏洞细节均来自 Parameter AI 于 2026 年 10 月 26 日的公开披露报告,该漏洞已由 OpenAI 修复。