沙箱
在序列化设计中,我们提到了“去脚本化”处理,即在回放时不会执行被录制页面中的任何 JavaScript,而是通过快照重现其效果。将 script 标签重写为 noscript 标签解决了一部分问题。然而,仍有一些脚本行为并不包含在 script 标签中,例如 HTML 中的内联脚本、表单提交等。
脚本行为种类繁多,采用过滤的方式来去除这些脚本永远不会是完整的解决方案,而且一旦有脚本漏网并被执行,就可能造成不可逆的意外后果。因此,我们使用 HTML 提供的 iframe 沙箱特性,在浏览器层面进行限制。
iframe 沙箱
重建快照时,我们会在一个 iframe 元素中重建录制的 DOM。通过设置它的 sandbox 属性,我们可以禁用以下行为:
- 表单提交
window.open等弹出窗口- JS 脚本(包括内联事件处理器和
javascript:URL)
这与我们的预期一致,尤其是在处理 JS 脚本时,它比我们自己实现这一安全措施更安全、更可靠。
rrweb-snapshot.rebuild() 会在浏览器环境中强制实施这一边界。在浏览器中进行重建时,应使用 rebuildIntoSandboxedIframe(),或使用由 createSandboxedIframe() 创建的 iframe——它会先创建一个恰好带有 sandbox="allow-same-origin" 的 iframe,然后再向其中重建内容。如果要直接对调用方创建的浏览器文档调用 rebuild(),则必须通过 UNSAFE_allowUnprotectedRebuild: true 明确选择进行无保护的重建。
避免链接跳转
当点击 a 元素链接时,默认行为是跳转到其 href 属性对应的 URL。在回放时,我们会通过重建跳转后的页面 DOM 来保证视觉上的正确回放,因此应当禁止原始的跳转。
通常,我们会通过事件代理捕获所有 a 元素的点击事件,并通过 event.preventDefault() 禁用默认行为。但当我们把回放页面放进沙箱后,所有事件处理器都不会执行,我们也就无法实现事件委托。
回放交互事件时需要注意,回放 JS click 事件并不是必需的,因为在 JS 被禁用的情况下,click 事件不会产生任何影响。不过,为了优化回放效果,我们可以为被鼠标点击的元素添加特殊的动画效果来进行可视化,清楚地向观看者表明此处发生了一次点击。
iframe 样式设置
由于我们在 iframe 中重建 DOM,因此无法通过父页面的 CSS 样式表影响 iframe 内的元素。但如果不允许执行 JS 脚本,noscript 标签就会被显示出来,而我们希望将其隐藏。因此我们需要动态地向 iframe 添加样式。示例代码如下:
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);
}请注意,这个插入的 style 元素并不存在于原始被录制页面中,因此我们不能对它进行序列化,否则 id -> Node 映射就会出错。