
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等属性表单提交form的submitjavascript:URLwindow.open之类的弹窗脚本化行为的种类繁多采用“过滤/白名单”的方式剔除它们永远不是一个完备的方案——只要有一条脚本漏网并在回放时被执行就可能触发不可逆的意外后果。因此 rrweb 选择利用 HTML 标准提供的iframe sandbox特性把安全边界下沉到浏览器本身而不是在应用层自行实现安全逻辑。这一判断在源码层面得到了印证rrweb 在浏览器环境中重建快照时默认强制要求目标文档位于受保护的沙箱 iframe 中直接向调用者创建的普通浏览器文档执行rebuild()会抛出错误详见下文“重建边界的强制校验”。iframe sandbox浏览器级别的行为禁用重建快照时rrweb 会把录制好的 DOM 放进一个iframe元素中通过设置其sandbox属性禁用以下行为表单提交form submission弹窗如window.openJS 脚本执行包括内联事件处理器与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 设置sandboxallow-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若调用方显式传入UNSAFE_allowUnprotectedRebuild: true则放行这是唯一的显式“无保护”退出通道否则要求目标文档必须已被登记进内部sandboxedRebuildDocuments这个WeakSetDocumentrebuild.ts L97且其frameElement必须是“恰好只有allow-same-origin一个 token”的 iframe——判断逻辑见isSupportedSandboxedIframe()rebuild.ts L157-L171不满足上述任一条件即抛出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时才会退化为手工创建sandboxallow-same-origin allow-scripts的 iframereplay/index.ts L618-L622因为 Canvas 回放需要执行录制脚本在canvas上的绘制命令——这属于面向高级场景的显式逃生舱。避免链接跳转无脚本环境下的交互事件回放回放时点击a链接其默认行为是跳转到href对应的 URL。rrweb 回放的目标是“视觉正确的重放”跳转后的页面 DOM 会由后续快照重建出来因此原始跳转必须被禁止。通常的做法是通过事件处理器代理捕获所有a元素的 click 事件并调用event.preventDefault()禁用默认行为。但问题在于把回放页面放进沙箱后所有事件处理器都不会执行没有脚本事件委托自然也无从谈起。这其实是沙箱策略带来的一个“良性副作用”——JS 被禁用后click事件本身不会对页面产生任何实际影响既不会触发跳转也不会有脚本响应。因此回放交互事件时rrweb 并不需要真的去派发 JSclick事件。在 replay/index.ts L1226-L1237 的鼠标交互处理中可以看到明确的注释与实现dont 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 click0.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); }要点有三样式必须插到iframe.contentDocument的head之前documentElement.insertBefore(styleEl, head)使其成为该文档自己的样式用CSSStyleSheet.insertRule()逐条插入规则规则编号idx即插入位置索引这条注入的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 回放安全可以归纳为三层防线序列化层去脚本化快照中script改写为noscript不记录脚本内容只记录脚本引发的 DOM 变化序列化设计浏览器层沙箱隔离通过sandboxallow-same-origin的 iframe 封死表单提交、弹窗与一切 JS 执行并由 rebuild.ts 的assertRebuildTargetAllowed()强制校验重建目标杜绝“无保护重建”需要打破默认安全策略时必须显式使用UNSAFE_allowUnprotectedRebuild或回放端的UNSAFE_replayCanvas配置回放层行为抑制与视觉补偿沙箱天然禁掉链接跳转等默认行为回放时不再派发真实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),仅供参考