ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

rrweb 沙箱化重建(Sandboxed Rebuild):用 iframe 沙箱强制浏览器重放安全边界

2026/9/20 5:12:25 拓冰建站 浏览量
rrweb 沙箱化重建(Sandboxed Rebuild):用 iframe 沙箱强制浏览器重放安全边界 前端可观测性开发工具【免费下载链接】rrwebrecord and replay the web项目地址https://gitcode.com/gh_mirrors/rr/rrweb点击查看免费下载本指南详解 rrweb 项目中一项关键安全架构决策——rrweb-snapshot.rebuild()默认拒绝在不受保护的浏览器文档中重建快照转而依靠 iframe sandbox 作为浏览器强制执行的脚本执行边界。读完本文你将理解该决策的来龙去脉、rebuild()守卫的具体实现原理、createSandboxedIframe()/rebuildIntoSandboxedIframe()安全辅助 API 的用法以及UNSAFE_replayCanvas显式不安全路径的迁移方式。决策背景直接调用rebuild()带来的执行漏洞rrweb 的序列化设计见 serialization.md遵循去脚本化de-scripting原则重放时不执行录制页面中的任何 JavaScript而是通过快照还原其视觉结果。例如script标签会被改写为noscript标签。然而脚本化行为远不止script标签一种——内联事件处理程序、javascript:URL、表单提交、SVG 中的可执行内容等都属于脚本化行为而这些并不能通过简单改写全部消除。正如仓库中 docs/sandbox.md 所记录的核心结论通过过滤filtering方式清除这些脚本永远不是完整方案一旦有脚本漏网并被执行就可能造成不可逆的意外后果。因此我们使用 HTML 提供的 iframe sandbox 特性做浏览器级别的限制。问题在于rrweb的重放器replayer一直遵守这一约定会先创建sandboxallow-same-origin的顶层 iframe 再重建快照但对外暴露的底层 APIrrweb-snapshot.rebuild()可以接收调用方提供的任意Document包括页面顶层 document。社区问题rrweb-io/rrweb#1817报告当调用方直接对未沙箱化的浏览器文档调用rebuild()时精心构造的重放内容可以执行脚本。这就是 0001-require-sandboxed-browser-rebuilds.md 这条 ADR架构决策记录要填补的缺口。核心决策用沙箱强制执行安全而非扩大净化范围ADR 给出的决策明确且克制rrweb-snapshot.rebuild()将默认拒绝在不受保护的浏览器文档中重建浏览器重放安全由iframe 沙箱强制执行而不是试图净化每一个可执行的 DOM、URL、CSS、SVG 或事件处理程序向量有意在不受保护的浏览器文档中重建的调用方必须使用显式的不安全 opt-out不可信的重放数据应使用沙箱化 iframe 目标或安全辅助 API。该决策刻意将修复保持在结构性层面不扩展属性或 URL 净化逻辑那是无底洞也不重新设计基于独立源或消息传递的渲染器而是把边界判定交给浏览器原生沙箱机制。完整的目标与非目标清单记录在设计文档 2026-05-18-sandboxed-rebuild-design.md 中其中明确将不尝试净化所有可执行 HTML/SVG/CSS/URL/事件处理器向量列为非目标。守卫实现rebuild()如何判定目标文档是否受保护判定函数与错误信息守卫逻辑全部集中在 packages/rrweb-snapshot/src/rebuild.ts 中由三个部分组成错误信息常量rebuild.ts#L92-L93const REBUILD_TARGET_ERROR 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.;沙箱 token 校验rebuild.ts#L157-L171目标文档所在的 frame 必须是IFRAME且其sandboxtoken 集合恰好等于[allow-same-origin]——多一个allow-scripts、allow-forms或少一个allow-same-origin都会被拒绝function isSupportedSandboxedIframe( frameElement: Element | null, ): frameElement is HTMLIFrameElement { if (!frameElement || frameElement.tagName ! IFRAME) return false; if (!(sandbox in frameElement)) return false; const sandboxTokens Array.from((frameElement as HTMLIFrameElement).sandbox); return sandboxTokens.length 1 sandboxTokens[0] allow-same-origin; }目标断言函数rebuild.ts#L173-L191先检查显式 opt-out然后要求目标文档存在有效的defaultView并且文档已登记在sandboxedRebuildDocuments弱集合中、其frameElement满足上述沙箱条件function assertRebuildTargetAllowed(options: RebuildOptions): void { if (options.UNSAFE_allowUnprotectedRebuild) return; const win options.doc.defaultView; if (!win) return; if ( sandboxedRebuildDocuments.has(options.doc) isSupportedSandboxedIframe(win.frameElement) ) { return; } throw new Error(REBUILD_TARGET_ERROR); }rebuild()主体在解构 options 之前就调用守卫rebuild.ts#L717-L721确保任何节点构建都不会发生在未受保护的目标文档上。需要重点理解的三条边界sandboxedRebuildDocuments弱集合是信任锚点只有通过createSandboxedIframe()创建的 iframe 文档才会被登记rebuild.ts#L784。这意味着即使调用方自己拼一个当前sandboxallow-same-origin的 iframe其文档也不会被信任——沙箱属性必须在 iframe 被附加/使用之前设置并由库自身强制才能防止先建后松的时序漏洞。doc.defaultView为空的场景跳过检查这是为无窗口环境如部分测试环境保留的例外见设计文档的 Error Handling And Compatibility 一节。守卫只属于rebuild()更低层的节点构建函数如buildNodeWithSN()在本次变更中不拥有浏览器沙箱策略的所有权避免把策略散落到每个构建节点。安全辅助 APIcreateSandboxedIframe与rebuildIntoSandboxedIframe为了让直接使用rrweb-snapshot的调用方有一等公民的安全路径包新增了两个导出index.ts#L25-L43。createSandboxedIframe创建并附加一个受保护重建专用的 iframerebuild.ts#L759-L787root必须是**已连接isConnected**到文档的元素否则抛出SANDBOXED_IFRAME_ROOT_ERROR逐项应用调用方传入的iframeAttributes但跳过sandbox属性——调用方无法覆盖或放宽被强制的沙箱策略最后强制iframe.setAttribute(sandbox, allow-same-origin)再附加到root创建后若拿不到contentDocument/contentWindow会回滚移除 iframe 并抛错成功后把文档登记进sandboxedRebuildDocuments。import { createSandboxedIframe } from rrweb-snapshot; const iframe createSandboxedIframe({ root: document.body, iframeAttributes: { title: Replay }, // sandbox 会被忽略并强制覆盖 }); // iframe.getAttribute(sandbox) allow-same-originrebuildIntoSandboxedIframe一步完成建 iframe 重建的首选辅助函数rebuild.ts#L798-L817const { iframe, node } rebuildIntoSandboxedIframe(snapshot, { root: document.body, cache, mirror, });它内部调用createSandboxedIframe()获取受保护 iframe然后以 iframe 文档为doc调用受守卫的rebuild()。返回{ iframe, node }其中node为Node | null与rebuild()的语义一致方便调用方继续调整 iframe 尺寸或管理生命周期。若重建失败新建的 iframe 会被移除后再重抛错误避免在页面上留下半成品。设计文档 2026-05-18-sandboxed-rebuild-design.md 同时强调该辅助函数要求显式root、不默认document.body、总是创建全新 iframe不接受调用方提供的 iframe且首版保持最小化不镜像 replayer 的样式、滚动、指针事件或注入样式行为。Replayer 集成UNSAFE_replayCanvas成为显式不安全路径rrweb重放器在 packages/rrweb/src/replay/index.ts 的setupDom()#L602-L640中保持原有默认 iframe 模型并接入新守卫默认路径调用createSandboxedIframe({ root: this.wrapper })创建重放 iframe获得sandboxallow-same-origin无需不安全 opt-out 即可满足守卫UNSAFE_replayCanvas: true路径手动创建 iframe 并设置sandboxallow-same-origin allow-scripts#L618-L622因为 canvas 重放需要脚本权限此时 iframe 不再满足安全守卫条件。因此重放器在重建首个全量快照时必须把 opt-out 与 canvas 模式绑定#L855rebuild(event.data.node, { doc: this.iframe.contentDocument, afterAppend, cache: this.cache, mirror: this.mirror, UNSAFE_allowUnprotectedRebuild: this.UNSAFE_replayCanvas, });实施计划 2026-05-18-sandboxed-rebuild.md 明确要求不得从其他调用点传递该不安全选项除非有测试证明该调用确实以不受保护的浏览器目标触达守卫路径。这也意味着开启UNSAFE_replayCanvas即主动放弃脚本执行保护属于开发者需要明确承担风险的选择。关于嵌套 iframe 的说明设计文档澄清本次修复不重写嵌套 iframe 的 sandbox 属性。在正常重放中顶层重放 iframe 的沙箱会约束其内部嵌套的浏览上下文因此被序列化的内层 iframe 无法重新启用祖先沙箱已移除的脚本执行能力——这次变更强化的是重建目标文档的边界而非逐一改写每个嵌套 iframe 的沙箱属性。迁移路径与兼容性这是一次对rrweb-snapshot的有意破坏性变更breaking change。对仍在使用旧行为——直接在未受保护的浏览器文档上调用rebuild()——的调用方迁移路径有两条对不可信重放内容改用rebuildIntoSandboxedIframe()或createSandboxedIframe()仅在明确接受脚本执行风险时设置UNSAFE_allowUnprotectedRebuild: truerebuild(node, { doc, cache, mirror, UNSAFE_allowUnprotectedRebuild: true, // 显式承担脚本执行风险 });该选项的价值在于让不安全用法在代码评审中可见同时不阻塞那些有意承担风险的既有测试或工具。官方发布说明changeset将此描述为对rrweb-snapshot的安全加固破坏性变更、对rrweb的 patch 级变更见实施计划 2026-05-18-sandboxed-rebuild.md 的 Task 5。测试验证用浏览器强制执行边界测试用例完整记录了守卫的预期行为见 packages/rrweb-snapshot/test/rebuild.test.tsjsdom 环境与 packages/rrweb/test/replayer.test.tsPuppeteer 驱动对顶层浏览器document调用rebuild()抛出守卫错误对调用方提供的 iframe 文档即使当前恰好是sandboxallow-same-origin同样抛出——因为其文档未被sandboxedRebuildDocuments登记对任何非恰好allow-same-origin的沙箱策略组合、allow-scripts、allow-same-origin allow-scripts、allow-same-origin allow-forms均抛出UNSAFE_allowUnprotectedRebuild: true允许显式承担风险的重建rebuildIntoSandboxedIframe()创建 iframe、重建前应用沙箱并返回重建节点且不允许调用方通过iframeAttributes.sandbox覆盖策略重放器在默认模式与UNSAFE_replayCanvas模式下分别产出allow-same-origin与allow-same-origin allow-scripts的沙箱属性且 canvas 模式在全量快照重建时走显式不安全路径。关键测试命令来自实施计划./node_modules/.bin/vitest run packages/rrweb-snapshot/test/rebuild.test.ts ./node_modules/.bin/cross-env PUPPETEER_HEADLESStrue ./node_modules/.bin/vitest run packages/rrweb/test/replayer.test.ts -t UNSAFE_replayCanvas|supported sandbox policy文档与配套材料该决策同步落地的文档包括docs/sandbox.md明确rebuild()在浏览器环境中强制执行沙箱边界浏览器重建应使用rebuildIntoSandboxedIframe()或createSandboxedIframe()直接对调用方创建的浏览器文档调用rebuild()必须显式 opt-outpackages/rrweb-snapshot/README.md将rebuild描述为低层 API默认要求沙箱化 iframe 目标并给出rebuildIntoSandboxedIframe的推荐用法guide.md更新UNSAFE_replayCanvas选项说明明确开启后重放 iframe 会加入allow-scripts并放弃沙箱脚本执行保护docs/recipes/canvas.md将原有启用 Canvas 重放会移除沙箱的警告升级为对脚本执行保护 opt-out 的完整说明。小结rrweb-snapshot的沙箱化重建决策体现了一条清晰的安全思路与其在净化所有可执行向量的军备竞赛中疲于奔命不如把边界判定交给浏览器 iframe 沙箱。rebuild()默认拒绝不受保护的浏览器目标、createSandboxedIframe()/rebuildIntoSandboxedIframe()提供一等安全路径、UNSAFE_allowUnprotectedRebuild与UNSAFE_replayCanvas让所有不安全用法在代码评审中显式可见——这一组合既堵住了直接调用底层 API 的漏洞又保留了高级用法的逃生舱是安全性与可用性之间的一次务实平衡。赞分享前端可观测性开发工具【免费下载链接】rrwebrecord and replay the web项目地址https://gitcode.com/gh_mirrors/rr/rrweb点击查看免费下载相关推荐gopherlings入门教程从hello world到接口实现的完整学习路径gopherlings入门教程从hello world到接口实现的完整学习路径 gopherlings是一个专为Go语言初学者设计的互动学习项目通过修复一系前端可观测性开发工具rrweb 沙箱化重建Sandboxed Rebuild让 rrweb-snapshot.rebuild() 默认拒绝非受保护浏览器文档rrweb 沙箱化重建Sandboxed Rebuild让 rrweb snapshot.rebuild 默认拒绝非受保护浏览器文档 本文基于仓库内实施计前端可观测性开发工具rrweb 回放沙箱机制解析用 iframe sandbox 隔离脚本行为与重建安全边界rrweb 回放沙箱机制解析用 iframe sandbox 隔离脚本行为与重建安全边界 在 rrweb 的录制 回放体系中回放阶段绝不会重新执行录制页面中前端可观测性开发工具上一篇2025 Codestral优化配置指南让AI模型在本地释放超强代码解释能力下一篇超全UX设计指南程序员必知的交互设计原则与落地实践创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考