rrweb 回放沙箱机制解析:用 iframe sandbox 隔离脚本行为与重建安全边界
2026/9/20 15:02:16 网站建设 项目流程

rrweb 回放沙箱机制解析:用 iframe sandbox 隔离脚本行为与重建安全边界

【免费下载链接】rrwebrecord and replay the web项目地址: https://gitcode.com/gh_mirrors/rr/rrweb

在 rrweb 的录制-回放体系中,回放阶段绝不会重新执行录制页面中的任何 JavaScript,而是通过快照数据“重现”脚本造成的 DOM 结果——这就是 序列化设计 中提到的de-scripting(去脚本化)过程。然而,单纯靠“过滤”手段剔除脚本永远无法做到 100% 完备,一旦有漏网脚本(如内联脚本、表单提交)在回放页面中被执行,就可能造成不可逆的意外后果。本文围绕 docs/sandbox.md 展开,讲解 rrweb 如何借助 HTML 标准的 iframesandbox特性在浏览器层面强制隔离脚本行为,并深入源码验证rrweb-snapshot的沙箱重建边界、回放端的链接跳转抑制与 iframe 内样式注入细节。读完本文,你将掌握 rrweb 回放安全模型的完整脉络,以及在自己的项目里正确使用rebuildIntoSandboxedIframe()/createSandboxedIframe()的安全姿势。

为什么需要浏览器级沙箱:de-scripting 的边界

在 序列化设计 中,rrweb 对录制页面做了“去脚本化”处理:快照中script标签会被改写成noscript占位标签,script内的内容不再重要,取而代之的是录制脚本所引发的 DOM 变化。这解决了一部分问题,但仍有脚本化行为并不包含在script标签内,典型的包括:

  • HTML 中的内联事件处理(onclick等属性)
  • 表单提交(formsubmit
  • javascript:URL
  • window.open之类的弹窗

脚本化行为的种类繁多,采用“过滤/白名单”的方式剔除它们永远不是一个完备的方案——只要有一条脚本漏网并在回放时被执行,就可能触发不可逆的意外后果。因此 rrweb 选择利用 HTML 标准提供的iframe sandbox特性,把安全边界下沉到浏览器本身,而不是在应用层自行实现安全逻辑。

这一判断在源码层面得到了印证:rrweb 在浏览器环境中重建快照时默认强制要求目标文档位于受保护的沙箱 iframe 中,直接向调用者创建的普通浏览器文档执行rebuild()会抛出错误(详见下文“重建边界的强制校验”)。

iframe sandbox:浏览器级别的行为禁用

重建快照时,rrweb 会把录制好的 DOM 放进一个<iframe>元素中,通过设置其sandbox属性禁用以下行为:

  • 表单提交(form submission)
  • 弹窗(如window.open
  • JS 脚本执行(包括内联事件处理器与javascript:URL)

这完全符合回放的预期:尤其是对 JS 脚本的处理,交给浏览器原生机制远比自己在应用层实现更安全、更可靠。

需要特别注意的是,sandbox属性是无痕限制——即使把 iframe 的src换成about:blank,限制依然生效。在 rrweb 中,回放端以about:blank的空白 iframe 起步,再将快照 DOM 重建进该 iframe 的contentDocument

两个辅助 API 与强制策略

在 packages/rrweb-snapshot/src/rebuild.ts 中,沙箱化重建被封装为两个导出工具:

  • createSandboxedIframe(options)(rebuild.ts L759-L787):创建并挂载一个专用于受保护重建的 iframe。它要求options.root必须已经连接到文档(否则抛出SANDBOXED_IFRAME_ROOT_ERROR,见 rebuild.ts L94-L95),随后强制给 iframe 设置sandbox="allow-same-origin"。源码注释明确说明:调用方传入的sandbox属性会被忽略,目的正是“防止回放数据削弱或扩大沙箱策略”(rebuild.ts L753-L754)。
  • rebuildIntoSandboxedIframe(snapshot, options)(rebuild.ts L798-L817):处理不可信回放数据的首选入口。它内部先调用createSandboxedIframe,再把快照重建进该 iframe 的contentDocument,返回{ iframe, node };若重建过程抛错,会先把刚挂载的 iframe 移除再重新抛出,避免留下半成品 DOM。

为什么是allow-same-origin而不是空值?因为 rrweb 回放过程中需要以同源身份访问 iframe 内的contentDocument来注入样式、派发交互事件;与此同时,不授予allow-scripts,从而彻底封死脚本执行通道。

重建边界的强制校验

从 rebuild.ts L92-L93 可以看到,rrweb 对“未受保护的浏览器文档重建”直接抛错:

rrweb-snapshot.rebuild() cannot rebuild into an unprotected browser document. Use rebuildIntoSandboxedIframe() or set UNSAFE_allowUnprotectedRebuild: true only when you accept the script-execution risk.

其实现路径是assertRebuildTargetAllowed()(rebuild.ts L173-L191):

  1. 若调用方显式传入UNSAFE_allowUnprotectedRebuild: true,则放行(这是唯一的显式“无保护”退出通道);
  2. 否则要求目标文档必须已被登记进内部sandboxedRebuildDocuments这个WeakSet<Document>(rebuild.ts L97),且其frameElement必须是“恰好只有allow-same-origin一个 token”的 iframe——判断逻辑见isSupportedSandboxedIframe()(rebuild.ts L157-L171);
  3. 不满足上述任一条件即抛出REBUILD_TARGET_ERROR

也就是说,sandboxedRebuildDocuments记录了哪些文档是经过createSandboxedIframe()正规渠道创建的,只有这些文档(或其宿主 iframe 策略完全匹配)才能被rebuild()接受。这一默认保护在测试中也有充分覆盖:rebuild()直接针对普通 document 会抛错、UNSAFE_allowUnprotectedRebuild: true可以显式放行、调用方尝试覆盖sandbox属性会被忽略、挂载 detached root 会拒绝创建 iframe 等,见 packages/rrweb-snapshot/test/rebuild.test.ts(如 L133-L149、L155、L247-L252、L286-L377 等用例)。

在 rrweb 主回放包中,setupDom()(packages/rrweb/src/replay/index.ts L602-L640)默认就走createSandboxedIframe({ root: this.wrapper })路径;仅当配置开启UNSAFE_replayCanvas时,才会退化为手工创建sandbox="allow-same-origin allow-scripts"的 iframe(replay/index.ts L618-L622),因为 Canvas 回放需要执行录制脚本在<canvas>上的绘制命令——这属于面向高级场景的显式逃生舱。

避免链接跳转:无脚本环境下的交互事件回放

回放时点击<a>链接,其默认行为是跳转到href对应的 URL。rrweb 回放的目标是“视觉正确的重放”:跳转后的页面 DOM 会由后续快照重建出来,因此原始跳转必须被禁止

通常的做法是通过事件处理器代理捕获所有<a>元素的 click 事件并调用event.preventDefault()禁用默认行为。但问题在于:把回放页面放进沙箱后,所有事件处理器都不会执行(没有脚本),事件委托自然也无从谈起。这其实是沙箱策略带来的一个“良性副作用”——JS 被禁用后,click事件本身不会对页面产生任何实际影响(既不会触发跳转,也不会有脚本响应)。

因此回放交互事件时,rrweb 并不需要真的去派发 JSclick事件。在 replay/index.ts L1226-L1237 的鼠标交互处理中可以看到明确的注释与实现:

don't wanttarget.click()here as could trigger an iframe navigation; instead any effects of the click should already be covered by mutations

也就是说,回放 click 时刻意不调用target.click()(防止引发 iframe 导航),因为点击的一切 DOM 效果早已由增量快照(mutation)覆盖;取而代之的做法是:通过移除再添加.activeclass(并借助void this.mouse.offsetWidth强制触发重绘)来触发 styles/style.css 中的click动画(@keyframes click,0.2 秒的扩散/淡出效果,见 style.css L25-L27、L55-L66)。这样既不会产生任何脚本副作用,又能通过视觉动效明确告诉观看者“这里发生过一次点击”,显著优化回放观感。对应地,Touch 事件则使用touch-active/touch-click动画(style.css L40-L48、L68 起)。

iframe 样式设置:隐藏 noscript 与动态注入规则

由于 DOM 重建在 iframe 内部进行,父页面的 CSS 样式表无法作用到 iframe 内的元素。而这里有一个必须处理的细节:JS 脚本被禁止执行后,序列化时由script改写而来的noscript标签会被浏览器正常渲染出来——这显然不是录制页面的真实外观,必须隐藏。

解决方案是向 iframe 文档动态注入样式。文档给出的示例代码(对应文档 docs/sandbox.md 的 iframe style settings 一节)如下:

const injectStyleRules: string[] = [ 'iframe { background: #f1f3f5 }', 'noscript { display: none !important; }', ]; const styleEl = document.createElement('style'); const { documentElement, head } = this.iframe.contentDocument!; documentElement!.insertBefore(styleEl, head); for (let idx = 0; idx < injectStyleRules.length; idx++) { (styleEl.sheet! as CSSStyleSheet).insertRule(injectStyleRules[idx], idx); }

要点有三:

  1. 样式必须插到iframe.contentDocument<head>之前(documentElement.insertBefore(styleEl, head)),使其成为该文档自己的样式;
  2. CSSStyleSheet.insertRule()逐条插入规则,规则编号idx即插入位置索引;
  3. 这条注入的<style>元素并不存在于原始录制页面中,因此绝不能把它序列化进快照,否则id -> Node映射会被破坏——这正是文档中特别强调的警告。

在 rrweb 主包中,这一思路被固化为 packages/rrweb/src/replay/styles/inject-style.ts 的rules(blockClass)工厂函数:它根据用户配置的blockClass生成.{blockClass} { background: currentColor }(用于屏蔽元素的高亮占位)以及noscript { display: none !important; }两条注入规则,回放端会在合适时机把这些规则注入 iframe 文档。可见文档示例与线上实现一脉相承。

小结:回放安全的三层防线

综合 docs/sandbox.md 与源码实现,rrweb 回放安全可以归纳为三层防线:

  1. 序列化层去脚本化:快照中script改写为noscript,不记录脚本内容,只记录脚本引发的 DOM 变化(序列化设计);
  2. 浏览器层沙箱隔离:通过sandbox="allow-same-origin"的 iframe 封死表单提交、弹窗与一切 JS 执行,并由 rebuild.ts 的assertRebuildTargetAllowed()强制校验重建目标,杜绝“无保护重建”;需要打破默认安全策略时,必须显式使用UNSAFE_allowUnprotectedRebuild或回放端的UNSAFE_replayCanvas配置;
  3. 回放层行为抑制与视觉补偿:沙箱天然禁掉链接跳转等默认行为,回放时不再派发真实click,转而用 CSS 动画呈现点击视觉反馈;同时向 iframe 动态注入样式隐藏noscript,保证与录制画面一致。

对于在自己的应用中集成 rrweb 回放能力的开发者,最安全的做法是始终使用rebuildIntoSandboxedIframe()处理不可信快照数据,避免直接对普通文档调用rebuild();只有在你完全清楚并接受脚本执行风险(如 Canvas 录制回放)时,才通过显式选项开启无保护重建。相关实现与验证代码可继续阅读 rebuild.ts、replay/index.ts 以及 rebuild.test.ts 中的沙箱测试用例。

【免费下载链接】rrwebrecord and replay the web项目地址: https://gitcode.com/gh_mirrors/rr/rrweb

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询