Skip to content

跨域 iframe

默认情况下,浏览器会让访问托管在不同域名下的 iframe 内容变得很困难。这是一项安全特性,用于防止恶意站点访问其他站点上的敏感信息。绕过这一安全特性是可行的,但除非你非常严格地只允许你信任的站点将你的网站嵌入 iframe 中,否则不建议这样做。 因为一旦允许录制跨域 iframe,任何恶意网站都可以嵌入你的网站,只要它们运行着 rrweb,就能录制你网站的全部内容。

如何录制跨域 iframe

在父页面中启用跨域 iframe 录制:

js
import { record } from '@rrweb/record';

record({
  emit(event) {}, // all events will be emitted here, including events from cross origin iframes
  recordCrossOriginIframes: true,
});

在子页面中启用跨域 iframe 回放:

js
import { record } from '@rrweb/record';

record({
  emit(event) {}, // this is required for rrweb, but the child page will not emit any events
  recordCrossOriginIframes: true,
});

注意事项

开启跨域 iframe 录制后,rrweb 会检查自身是否运行在顶级窗口中。 如果不是,它会通过 postMessage 将事件发送给父窗口。

如果顶级窗口中没有运行 rrweb,那么开启 recordCrossOriginIframes 后事件将会丢失。

如果顶级窗口是恶意网站,它可以监听这些事件,并将其发送到它选择的服务器。

或者,如果你的页面上运行着恶意脚本,它们也可以监听 postMessage;由于子窗口与父窗口之间的通信未加密,它们能够看到这些事件。

将 rrweb 注入跨域 iframe 的方式

1. 网站所有者:在 iframe 中添加 rrweb

如果你同时拥有包含 iframe 的网站和被嵌入 iframe 的网站,可以通过 script 标签在两个页面中都添加 rrweb。

2. 浏览器扩展

参见 https://developer.chrome.com/docs/extensions/mv3/content_scripts/#functionality

3. Puppeteer 脚本

js
import puppeteer from 'puppeteer';

async function injectRecording(frame) {
  await frame.evaluate((rrwebCode) => {
    if (window.__IS_RECORDING__) return;
    window.__IS_RECORDING__ = true;

    (async () => {
      function loadScript(code) {
        const s = document.createElement('script');
        s.type = 'text/javascript';
        s.innerHTML = code;
        if (document.head) {
          document.head.append(s);
        } else {
          requestAnimationFrame(() => {
            document.head.append(s);
          });
        }
      }
      loadScript(rrwebCode);

      window.rrweb.record({
        emit: (event) => {
          window._captureEvent(event);
        },
        recordCrossOriginIframes: true,
      });
    })();
  }, code);
}

const browser = await puppeteer.launch();
const page = (await browser.pages())[0];

const events = []; // contains all events from all frames

await page.exposeFunction('_captureEvent', (event) => {
  events.push(event);
});

page.on('framenavigated', async (frame) => {
  await injectRecording(frame); // injects rrweb into the iframe
});

await page.goto('https://example.com');

// your events are in the events array

4. Electron

ts
const win = new BrowserWindow({
  width: 800,
  height: 600,
  webPreferences: {
    preload: path.join(__dirname, 'rrweb-recording-script.js'),
    // this turns on preload inside iframes, but disables node integration
    nodeIntegrationInSubFrames: true,
    nodeIntegration: false,
  },
});

参见 https://www.electronjs.org/docs/latest/tutorial/tutorial-preload 以及 https://www.electronjs.org/docs/latest/tutorial/process-model#preload-scripts