Skip to content

Session replay: observeAttachShadow throws SecurityError on cross-origin iframes (unguarded contentWindow.Element) #5148

Closed

Description

@vedamayank

Bug

The recorder's iframe load listener can throw an uncaught SecurityError from inside rrweb:

SecurityError: Failed to read a named property 'Element' from 'Window': Blocked a frame with origin "https://<site>" from accessing a cross-origin frame.
  at HTMLIFrameElement.<onSubsequentLoad>   posthog-recorder.js
  at <onceIframeLoaded listener>            posthog-recorder.js
  at onIframeLoad                           posthog-recorder.js
  at ShadowDomManager.observeAttachShadow   posthog-recorder.js

We see it on posthog-js 1.417.4 and 1.419.4 (Chrome 151 desktop, Chrome Mobile 139 Android).
The iframe is Firebase Auth's hidden gapi iframe (iframe#I0_<timestamp>, cross-origin to the page), and once an extension's iframe.

Where

observeAttachShadow checks contentWindow and contentDocument and then reads iframeElement.contentWindow.Element with no try/catch:

observeAttachShadow(iframeElement) {
  if (!iframeElement.contentWindow || !iframeElement.contentDocument) return;
  this.patchAttachShadow(iframeElement.contentWindow.Element, iframeElement.contentDocument);
}

It is unchanged in the latest release (1.434.18).
IframeManager.attachIframe already wraps its own cross-origin window access in callSafely (after #2533), but observeAttachShadow, which onIframeLoad calls right after it, does not.

Related: blockSelector does not stop it

We block the iframe with session_recording.blockSelector: 'iframe[src*="__/auth/"]', and it matches (the src is set when the iframe is inserted).
serializeNodeWithId still registers onceIframeLoaded for every iframe, so the load listener and onIframeLoad run for blocked iframes too.

Suggested fix

  1. Wrap observeAttachShadow (or the whole onIframeLoad callback) in callSafely.
  2. Skip onceIframeLoaded for iframes serialized with needBlock, since nothing of a blocked iframe is recorded.

Impact

Nothing visible breaks, but the throw escapes to the page's error handlers (Sentry, window.onerror), and every recorder release regroups it into a new issue.

Activity

  1. github-actions commented on Sep 29, 2026

    @github-actions
    Contributor

    AI triage for #5148

    Conclusion: Confirmed session-replay recorder bug: a cross-origin navigation can leave observeAttachShadow reading contentWindow.Element without the existing SecurityError containment.
    Confidence: 97%
    Suggested labels: feature/replay, javascript, needs-to-be-contributed-back, bug

    Thanks — confirmed from the current recorder source.

    ShadowDomManager.observeAttachShadow() checks contentWindow and contentDocument, then accesses contentWindow.Element outside a SecurityError guard. Both full-snapshot and mutation iframe-load callbacks invoke it immediately after IframeManager.attachIframe().

    The proposed blocked-iframe change is also warranted: serializeNodeWithId() stops serializing a blocked iframe's children via needBlock, but still unconditionally registers onceIframeLoaded. A later load callback can therefore still enter iframe serialization and shadow setup for an iframe selected by blockSelector.

    A minimal fix should preserve existing failure semantics by suppressing only DOMException SecurityErrors (as callSafely already does), while continuing to surface unrelated exceptions. Since this is a generic rrweb iframe-access issue in the vendored fork, it should also be considered for an upstream rrweb contribution.

    Investigation

    • observeAttachShadow() directly evaluates iframeElement.contentWindow.Element after truthiness checks; a cross-origin WindowProxy permits obtaining contentWindow but throws SecurityError when its named Element property is read.
    • The full-snapshot callback in record/index.ts and mutation callback in record/mutation.ts both call shadowDomManager.observeAttachShadow(iframe) after attaching an iframe.
    • IframeManager.attachIframe() already uses callSafely() around cross-origin window event-listener access. callSafely() specifically consumes DOMException errors named SecurityError and rethrows all other errors, which is the appropriate existing behavior to reuse.
    • In serializeNodeWithId(), needBlock prevents ordinary child traversal, but iframe handling occurs later and unconditionally calls onceIframeLoaded(). Thus a block-selected iframe still receives a deferred load listener.
    • Current tests cover same-origin iframe shadow-DOM observation and several iframe SecurityError cases in IframeManager, but do not cover a SecurityError thrown when observeAttachShadow() reads contentWindow.Element, nor listener omission for a blocked iframe.
    • Replay incident review: this changes recorder code delivered through the lazy recorder path, but it introduces no core/extension API, persisted-state, or signature dependency and should not affect recording volume. The main remaining risk is fleet-wide recorder behavior, so regression coverage should be included.

    Relevant files: packages/rrweb/rrweb/src/record/shadow-dom-manager.ts, packages/rrweb/rrweb/src/record/index.ts, packages/rrweb/rrweb/src/record/mutation.ts, packages/rrweb/rrweb/src/record/iframe-manager.ts, packages/rrweb/rrweb/src/utils.ts, packages/rrweb/rrweb-snapshot/src/snapshot.ts, packages/rrweb/rrweb/test/record/shadow-dom-manager.test.ts, packages/rrweb/rrweb-snapshot/test/snapshot.test.ts, .agents/skills/replay-incident-risk/INCIDENTS.md

    Fix assessment

    • Straightforward: no
    • Risk: medium
    • Reason: The defect is localized to one unguarded cross-origin WindowProxy access, with a reusable helper that retains non-SecurityError failures. Avoiding deferred iframe listeners for needBlock is likewise a narrow serializer change consistent with blocked-element behavior.
    • Suggested approach: Wrap the complete cross-origin access path in ShadowDomManager.observeAttachShadow() with the existing callSafely() helper. In serializeNodeWithId(), retain the iframe's needBlock state long enough to skip onceIframeLoaded() and onIframeListenerRegistered for blocked iframes. Add focused regression tests: one supplying an iframe whose contentWindow.Element getter throws DOMException('SecurityError'), and one asserting a block-selected iframe does not register a load listener. If practical, extend the existing real-browser cross-origin iframe coverage to assert no page error during an iframe navigation.

    Pi session

    The resumable JSONL pi session for this run was saved to a private gist: https://gist.github.com/marandaneto/2fdc6a854e210938b4d2a35b14c29d88

    Download it locally, then fork it into your own pi session:

    pi --fork path/to/session.jsonl

    Saved session:

    Close proposal

    None. No issue close action was taken.


    Generated by posthog-watcher-action using pi.

  2. added
    bugSomething isn't working
    javascriptPull requests that update javascript code
    needs-to-be-contributed-backrrweb fork change that should be contributed back to upstream rrweb-io/rrweb
    on Sep 29, 2026
  3. deepanshupal commented on Sep 29, 2026

    @deepanshupal
    Contributor

    I can handle the recorder exception with a narrow guard around observeAttachShadow and tests for cross-origin SecurityError, unrelated errors, and same-origin behavior. I won't change blocked-iframe listener registration in this patch. I noticed PostHog/posthog-rrweb is archived; where would you like the matching rrweb fix contributed?

  4. dustinbyrne commented on Sep 29, 2026

    @dustinbyrne
    Contributor

    thanks @deepanshupal - we'll take a look shortly!

    where would you like the matching rrweb fix contributed

    changes can be made to the rrweb package in this repository, and updates ship via posthog-js when creating a changeset 🙌

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingfeature/replayjavascriptPull requests that update javascript codeneeds-to-be-contributed-backrrweb fork change that should be contributed back to upstream rrweb-io/rrweb

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions