You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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:
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
Wrap observeAttachShadow (or the whole onIframeLoad callback) in callSafely.
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.
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 DOMExceptionSecurityErrors (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.
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.
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?
Activity
github-actions commentedon Sep 29, 2026
AI triage for #5148
Conclusion: Confirmed session-replay recorder bug: a cross-origin navigation can leave
observeAttachShadowreadingcontentWindow.Elementwithout the existingSecurityErrorcontainment.Confidence: 97%
Suggested labels:
feature/replay,javascript,needs-to-be-contributed-back,bugThanks — confirmed from the current recorder source.
ShadowDomManager.observeAttachShadow()checkscontentWindowandcontentDocument, then accessescontentWindow.Elementoutside aSecurityErrorguard. Both full-snapshot and mutation iframe-load callbacks invoke it immediately afterIframeManager.attachIframe().The proposed blocked-iframe change is also warranted:
serializeNodeWithId()stops serializing a blocked iframe's children vianeedBlock, but still unconditionally registersonceIframeLoaded. A later load callback can therefore still enter iframe serialization and shadow setup for an iframe selected byblockSelector.A minimal fix should preserve existing failure semantics by suppressing only
DOMExceptionSecurityErrors (ascallSafelyalready 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 evaluatesiframeElement.contentWindow.Elementafter truthiness checks; a cross-origin WindowProxy permits obtainingcontentWindowbut throwsSecurityErrorwhen its namedElementproperty is read.record/index.tsand mutation callback inrecord/mutation.tsboth callshadowDomManager.observeAttachShadow(iframe)after attaching an iframe.IframeManager.attachIframe()already usescallSafely()around cross-origin window event-listener access.callSafely()specifically consumesDOMExceptionerrors namedSecurityErrorand rethrows all other errors, which is the appropriate existing behavior to reuse.serializeNodeWithId(),needBlockprevents ordinary child traversal, but iframe handling occurs later and unconditionally callsonceIframeLoaded(). Thus a block-selected iframe still receives a deferred load listener.SecurityErrorcases inIframeManager, but do not cover aSecurityErrorthrown whenobserveAttachShadow()readscontentWindow.Element, nor listener omission for a blocked iframe.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.mdFix assessment
needBlockis likewise a narrow serializer change consistent with blocked-element behavior.ShadowDomManager.observeAttachShadow()with the existingcallSafely()helper. InserializeNodeWithId(), retain the iframe'sneedBlockstate long enough to skiponceIframeLoaded()andonIframeListenerRegisteredfor blocked iframes. Add focused regression tests: one supplying an iframe whosecontentWindow.Elementgetter throwsDOMException('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:
Saved session:
call-1-1-2026-09-29T18-04-16-628Z_01a0ee56-bb74-768d-9af6-eca0eee90d3a.jsonlClose proposal
None. No issue close action was taken.
Generated by posthog-watcher-action using pi.
deepanshupal commentedon Sep 29, 2026
I can handle the recorder exception with a narrow guard around
observeAttachShadowand tests for cross-origin SecurityError, unrelated errors, and same-origin behavior. I won't change blocked-iframe listener registration in this patch. I noticedPostHog/posthog-rrwebis archived; where would you like the matching rrweb fix contributed?dustinbyrne commentedon Sep 29, 2026
thanks @deepanshupal - we'll take a look shortly!
changes can be made to the rrweb package in this repository, and updates ship via
posthog-jswhen creating a changeset 🙌