Skip to content

Cross-origin iframe recording: origin-scoped collectFromOrigins / emitToOrigin - #1991

Open
eoghanmurray wants to merge 5 commits into
mainfrom
cross-origin-collect-emit-origins
Open

eoghanmurray wants to merge 5 commits into
mainfrom
cross-origin-collect-emit-origins

Conversation

@eoghanmurray

Copy link
Copy Markdown
Collaborator

Alternative to #1679. The first four commits here are #1679's own commits (by @econte-sprig and @Juice10), cherry-picked onto current main so this branch is reviewable standalone; the final commit builds the alternative design on top. If this direction is preferred, those commits can be cherry-picked back onto #1679.

Motivation

#1679 adds crossOriginIframeParentOrigin to harden the send side of cross-origin iframe recording (the child's postMessage target origin). While reviewing it, a broader issue surfaced: the single recordCrossOriginIframes switch conflates two independent roles whose correct trust boundary differs, and it offers no control over trust in either direction:

  • Receive side (parent): with the flag on, the parent accepts forwarded events from any embedded iframe it has in the DOM — including ad frames or untrusted widgets, which can then inject fabricated events into the recording.
  • Send side (child): the child forwards to *, so any embedder can harvest the recording.

crossOriginIframeParentOrigin only addresses the second. This PR addresses both, symmetrically.

Change

Replace the single switch with two origin-scoped options, each self-enabling by its presence:

option side meaning
collectFromOrigins: string[] parent accept & trust forwarded events only from child iframes on these origins; everything else is dropped in IframeManager. ['*'] opts back into accept-any.
emitToOrigin: string child forward this frame's events to its parent, using this value as the postMessage target origin so the browser only delivers to a matching parent.

A frame in the middle of a nested chain sets both — targeting is per-immediate-parent, not the root, matching how events hop one level at a time.

This supersedes crossOriginIframeParentOrigin (removed here, since #1679 is unreleased) and deprecates recordCrossOriginIframes, which still works and resolves to collectFromOrigins: ['*'] plus forward-to-any. The legacy auto-role path (including the same-origin-child no-op) is kept intact.

Docs & tests

  • Recipe (docs/recipes/cross-origin-iframes.md) and the guide.md options table rewritten around the two options, with a nested-iframe example and a recordCrossOriginIframes migration section (en + zh_CN).
  • New tests: cross-origin-iframe-emit-to-origin.test.ts (send-side target-origin gate) and cross-origin-iframe-collect-from-origins.test.ts (receive-side origin allow-list).
  • The existing cross-origin-iframes.test.ts (22 tests) still passes unchanged, confirming the deprecated path is preserved.

🤖 Generated with Claude Code

@changeset-bot

changeset-bot Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: d253958

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 22 packages
Name Type
rrweb Patch
@rrweb/record Patch
@rrweb/all Patch
@rrweb/browser-client Patch
@rrweb/packer Patch
@rrweb/replay Patch
rrdom-nodejs Patch
rrdom Patch
rrvideo Patch
rrweb-player Patch
rrweb-snapshot Patch
@rrweb/types Patch
@rrweb/utils Patch
@rrweb/web-extension Patch
@rrweb/rrweb-plugin-canvas-webrtc-record Patch
@rrweb/rrweb-plugin-canvas-webrtc-replay Patch
@rrweb/rrweb-plugin-console-record Patch
@rrweb/rrweb-plugin-console-replay Patch
@rrweb/rrweb-plugin-network-record Patch
@rrweb/rrweb-plugin-network-replay Patch
@rrweb/rrweb-plugin-sequential-id-record Patch
@rrweb/rrweb-plugin-sequential-id-replay Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new

pkg-pr-new Bot commented Oct 28, 2026 •

Copy link
Copy Markdown
@rrweb/all

npm i https://pkg.pr.new/rrweb-io/rrweb/@rrweb/all@d253958

@rrweb/browser-client

npm i https://pkg.pr.new/rrweb-io/rrweb/@rrweb/browser-client@d253958

@rrweb/packer

npm i https://pkg.pr.new/rrweb-io/rrweb/@rrweb/packer@d253958

@rrweb/record

npm i https://pkg.pr.new/rrweb-io/rrweb/@rrweb/record@d253958

@rrweb/replay

npm i https://pkg.pr.new/rrweb-io/rrweb/@rrweb/replay@d253958

rrdom

npm i https://pkg.pr.new/rrweb-io/rrweb/rrdom@d253958

rrdom-nodejs

npm i https://pkg.pr.new/rrweb-io/rrweb/rrdom-nodejs@d253958

rrvideo

npm i https://pkg.pr.new/rrweb-io/rrweb/rrvideo@d253958

rrweb

npm i https://pkg.pr.new/rrweb-io/rrweb@d253958

rrweb-player

npm i https://pkg.pr.new/rrweb-io/rrweb/rrweb-player@d253958

rrweb-snapshot

npm i https://pkg.pr.new/rrweb-io/rrweb/rrweb-snapshot@d253958

@rrweb/types

npm i https://pkg.pr.new/rrweb-io/rrweb/@rrweb/types@d253958

@rrweb/utils

npm i https://pkg.pr.new/rrweb-io/rrweb/@rrweb/utils@d253958

@rrweb/rrweb-plugin-canvas-webrtc-record

npm i https://pkg.pr.new/rrweb-io/rrweb/@rrweb/rrweb-plugin-canvas-webrtc-record@d253958

@rrweb/rrweb-plugin-canvas-webrtc-replay

npm i https://pkg.pr.new/rrweb-io/rrweb/@rrweb/rrweb-plugin-canvas-webrtc-replay@d253958

@rrweb/rrweb-plugin-console-record

npm i https://pkg.pr.new/rrweb-io/rrweb/@rrweb/rrweb-plugin-console-record@d253958

@rrweb/rrweb-plugin-console-replay

npm i https://pkg.pr.new/rrweb-io/rrweb/@rrweb/rrweb-plugin-console-replay@d253958

@rrweb/rrweb-plugin-network-record

npm i https://pkg.pr.new/rrweb-io/rrweb/@rrweb/rrweb-plugin-network-record@d253958

@rrweb/rrweb-plugin-network-replay

npm i https://pkg.pr.new/rrweb-io/rrweb/@rrweb/rrweb-plugin-network-replay@d253958

@rrweb/rrweb-plugin-sequential-id-record

npm i https://pkg.pr.new/rrweb-io/rrweb/@rrweb/rrweb-plugin-sequential-id-record@d253958

@rrweb/rrweb-plugin-sequential-id-replay

npm i https://pkg.pr.new/rrweb-io/rrweb/@rrweb/rrweb-plugin-sequential-id-replay@d253958

commit: d253958

@eoghanmurray
eoghanmurray force-pushed the cross-origin-collect-emit-origins branch from f2ccc6c to d35a4c4 Compare October 28, 2026 23:30
The single `recordCrossOriginIframes` switch conflated two independent,
position-inferred roles and gave no control over trust: a collector accepted
forwarded events from *any* embedded iframe (ads, untrusted widgets could
inject fabricated events), and a child forwarded to `*` (any embedder could
harvest the recording).

Split it into two origin-scoped options, each self-enabling by presence:

- `collectFromOrigins: string[]` (parent side) — accept and trust forwarded
  events only from child iframes on these origins; drop everything else in
  IframeManager before it enters the recording. `['*']` opts back into
  accept-any.
- `emitToOrigin: string` (child side) — forward this frame's events to its
  parent using this value as the postMessage targetOrigin, so the browser
  only delivers to a matching parent. A middle frame in a nested chain sets
  both; targeting is per-immediate-parent, not the root.

Supersedes the unreleased `crossOriginIframeParentOrigin` from this PR's base,
which is removed outright (it only hardened the send half). `recordCrossOriginIframes`
is deprecated but still supported, resolving to `collectFromOrigins: ['*']`
plus forward-to-any — the legacy auto-role path (incl. the same-origin-child
no-op) is kept intact and still passes its 22 existing tests.

Docs (recipe + guide tables, en + zh_CN) rewritten around the two options with
a migration section. New tests cover the send-side targetOrigin gate and the
receive-side origin allow-list.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@eoghanmurray
eoghanmurray force-pushed the cross-origin-collect-emit-origins branch from d35a4c4 to d253958 Compare October 28, 2026 23:31
@eoghanmurray

Copy link
Copy Markdown
Collaborator Author

re. versioning:

The semver.org FAQ explicitly says to ship a deprecation as a minor release: update the docs, mark it deprecated, and cut a minor so consumers get a signal on their next minor upgrade. The reasoning is that the public API surface has meaningfully changed (something is now on notice), even though nothing breaks and nothing is removed.

So by the book, this PR had two separate pulls toward minor:

  1. Two new options added (collectFromOrigins, emitToOrigin) — additive API = minor.
  2. recordCrossOriginIframes deprecated — deprecation = minor per the FAQ.

The counter-argument for patch is that it's a convention/recommendation, not a hard rule: a deprecation changes zero runtime behavior (the option still works identically), and here it's just a JSDoc @deprecated tag plus a doc note — no runtime warning. Plenty of projects fold that into a patch.

Comment on lines +15 to +22
> **Security warning:** Cross-origin iframe recording is not recommended unless
> you [strictly restrict](https://stackoverflow.com/a/21629575) which sites can
> embed your page and trust those sites with its recorded contents. The
> browser's cross-origin restrictions protect sensitive information. Accepting
> events with `collectFromOrigins: ['*']`, or forwarding them with
> `emitToOrigin: '*'`, removes that protection — a malicious page can then
> embed your page and collect its recorded contents, or inject fabricated
> events into your recording. Name specific origins on both sides.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some mixed messages above.

For completeness, here is my understanding of all this:

It is safe to have emitToOrigin: '*' as long as you follow the guidelines here: https://stackoverflow.com/a/21629575 and strictly restrict who can embed your webpage in an iframe.

On the parent side, if you are iframing a malicious website, which is probably most relevant when a domain expires and is bought by a malicious actor, you run the risk of them filling your recordings with junk if you have collectFromOrigins: ['*'], and there might be a risk of them corrupting your recordings. And an unknown risk of them breaking out of the sandbox on replay side in the case of canvas replay being on which is more risky.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants