Cross origin iframe support (#1035)

* Add `recordCrossOriginIframe` setting

* Set up messaging between iframes

* should emit full snapshot event from iframe as mutation event

* this.mirror was dropped on attachIframe

* should use unique id for child of iframe

* Cross origin iframe recording in `yarn live-stream`

* Root iframe check thats supported by firefox

* Live stream: Inject script in all frames

* Record same origin and cross origin iframes differently

* Should map Input events correctly

* Turn on other tests

* Fix compatibility with newer puppeteer

* puppeteer vs 12 seems stable without to many changes needed

* normalize port numbers in snapshots

* Handle scroll and ViewportResize events in cross origin iframe

* Correctly map cross origin mutations

* Map selection events for cross origin iframes

* Map canvas mutations for cross origin iframes

* Update snapshot to include canvas events

* Skip all meta events

* Support custom events as best we can in cross origin iframes

* Use earliest version of puppeteer that works with cross origin live-stream

* Map mouse/touch interaction events

* Update snapshots for correctly mapped click events

* Tweak tests for new puppeteer version

* Map MediaInteraction correctly for cross origin iframes

* Make tests consistent between high and low dpi devices

* Make test less flaky

* Make test less flaky

* Make test less flaky

* Make test less flaky

* Add support for styles in cross origin iframes

* Map traditional stylesheet mutations on cross origin iframes

* Add todo

* Add iframe mirror

* Get iframe manager to use iframe mirrors internally

* Rename `IframeMirror` to `CrossOriginIframeMirror`

* Setup basic cross origin canvas webrtc streaming

* Clean up removed canvas elements

* reset style mirror on new full snapshot

* Fix cross origin canvas webrtc streaming

* Make emit optional

* Run tests on github actions

* Upload image artifacts from failed tests

* Use newer github actions

* Test: hopefully adding more wait will fix it

* add extra wait

* Fix image snapshot tests

* Make tests run with new puppeteer version

* upgrade eslint-plugin-jest

* Chore: Remove travis ci as ci's running on github actions

* Chore: Support recording cross origin iframe in repl

* Force developers to update the cross origin iframe mapping when adding new events

https://github.com/rrweb-io/rrweb/pull/1035#discussion_r1012516277

* Document cross origin iframe recording

* Docs: cross origin iframes recording methods

* Docs: AI translated, cross origin iframe recording

* rename getParentId to getId

* Migrate to @rrweb/types

* Run on pull request

* doc: improve Chinese doc

* Rename `parentId` to `Id`

Co-authored-by: Mark-Fenng <f18846188605@gmail.com>
This commit is contained in:
Justin Halsall
2026-04-01 12:00:00 +08:00
committed by GitHub
parent df5d547446
commit 2cd3d2afe9
38 changed files with 7362 additions and 1038 deletions

View File

@@ -0,0 +1,116 @@
# Cross origin iframes
By default browsers make it difficult to access the contents of an iframe that is hosted on a different domain. This is a security feature to prevent malicious sites from accessing sensitive information on other sites. It is possible to work around this security feature, but it is not recommended unless you [are very strict](https://stackoverflow.com/a/21629575) about allowing only the sites you trust to embed your website inside of an iframe.
Since if you allow recording cross origin iframes, any malicious website can embed your website and as long as they have rrweb running they can record all the contents of your website.
## How to record cross origin iframes
Enable recording cross-origin iframes in your parent page:
```js
rrweb.record({
emit(event) {}, // all events will be emitted here, including events from cross origin iframes
recordCrossOriginIframes: true,
});
```
Enable replaying cross-origin iframes in your child page:
```js
rrweb.record({
emit(event) {}, // this is required for rrweb, but the child page will not emit any events
recordCrossOriginIframes: true,
});
```
## Considerations
When cross origin iframe recording is turned on rrweb will check to see if it is being run in a top level window.
If it isn't it'll send the events to the parent window via `postMessage`.
If you don't have rrweb running in the top level window, the events will be lost when `recordCrossOriginIframes` is turned on.
If the top level window is a malicious website it can listen to the events and send them to a server of its choosing.
Or if a malicious script is running in on your page they can listen in on `postMessage` and as communication between the child and parent window is not encrypted. And they can see the events.
## Options for injecting rrweb into cross origin iframes
### 1. Website owners, add rrweb in the iframes
If you own the website that with the iframe and the website that is being embedded in an iframe, you can add rrweb to both pages via a script tag.
### 2. Browser extension
See https://developer.chrome.com/docs/extensions/mv3/content_scripts/#functionality
### 3. Puppeteer script
```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,
},
});
```
See https://www.electronjs.org/docs/latest/tutorial/tutorial-preload
And https://www.electronjs.org/docs/latest/tutorial/process-model#preload-scripts

View File

@@ -0,0 +1,113 @@
# 跨域 IFrame 录制
默认情况下,浏览器很难访问托管在不同域上的 iframe 的内容。 这是一项安全功能,可防止恶意站点访问其他站点上的敏感信息。 rrweb 提供了一个解决方案但如果您对网站的安全有严格的要求https://stackoverflow.com/a/21629575 只允许您信任的网站将您的网站嵌入 iframe 中,那我们不建议使用我们的解决方案。
因为如果您允许记录跨源 iframe任何恶意网站都可以嵌入您的网站并且只要它们运行 rrweb它们就可以记录您网站的所有内容。
## 如何记录跨源 iframe
在父页面中启用录制跨域 iframe:
```js
rrweb.record({
emit(event) {}, // 所有事件都将在此处发出,包括来自跨源 iframe 的事件
recordCrossOriginIframes: true,
});
```
在您的子页面中启用重放跨域 iframe:
```js
rrweb.record({
emit(event) {}, // 这是 rrweb 所必需的,但子页面不会发出任何事件
recordCrossOriginIframes: true,
});
```
## 注意事项
当跨源 iframe 录制开启时rrweb 将检查它是否正在顶级窗口中运行。 如果不是,它将通过 `postMessage` 将事件发送到父窗口。
如果您没有在顶层窗口中运行 rrweb则打开 `recordCrossOriginIframes` 时事件将丢失。
如果顶层窗口是一个恶意网站它可以监听事件并将它们发送到它选择的服务器。或者如果您的页面上正在运行恶意脚本则它们可以监听“postMessage”又因为子窗口和父窗口之间的通信未加密所以他们可以获取到所有事件。
## 将 rrweb 注入跨源 iframe 的选项
### 1. 网站所有者,在 iframe 中添加 rrweb
如果您拥有使用 iframe 的网站和嵌入在 iframe 中的网站,您可以通过脚本标签将 rrweb 添加到这两个页面。
### 2. 浏览器扩展
请查看 https://developer.chrome.com/docs/extensions/mv3/content_scripts/#functionality
### 3. Puppeteer script
```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 = []; // 包含来自所有Frame的所有事件
await page.exposeFunction('_captureEvent', (event) => {
events.push(event);
});
page.on('framenavigated', async (frame) => {
await injectRecording(frame); // 将 rrweb 注入 iframe
});
await page.goto('https://example.com');
// 您的事件将在事件数组中
```
### 4. Electron
```ts
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'rrweb-recording-script.js'),
// 这会打开 iframe 内的预加载,但会禁用节点集成
nodeIntegrationInSubFrames: true,
nodeIntegration: false,
},
});
```
请查看 https://www.electronjs.org/docs/latest/tutorial/tutorial-preload
和 https://www.electronjs.org/docs/latest/tutorial/process-model#preload-scripts

View File

@@ -67,4 +67,4 @@ source -> IncrementalSource.Font
data -> fontData
```
enum IncrementalSource's definition can be found in this [list](https://github.com/rrweb-io/rrweb/blob/master/packages/rrweb/typings/types.d.ts#L62).
enum IncrementalSource's definition can be found in this [list](https://github.com/rrweb-io/rrweb/blob/98e71cd0d23628cd1fbdbe47664a65748084c4a4/packages/types/src/index.ts#L69).