Skip to content
This repository was archived by the owner on May 7, 2026. It is now read-only.
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 17 additions & 4 deletions packages/rrweb/src/record/observer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -883,10 +883,23 @@ export function initAdoptedStyleSheetObserver(
// The host is a ShadowRoot.
else hostId = mirror.getId(dom.host(host as ShadowRoot));

const patchTarget =
host.nodeName === '#document'
? (host as Document).defaultView?.Document
: host.ownerDocument?.defaultView?.ShadowRoot;
// Reading `ownerDocument` on a ShadowRoot whose owning document has navigated
// cross-origin can throw a SecurityError in Firefox; bail out cleanly so the
// caller is not aborted.
let patchTarget: typeof Document | typeof ShadowRoot | undefined;
try {
patchTarget =
host.nodeName === '#document'
? (host as Document).defaultView?.Document
: host.ownerDocument?.defaultView?.ShadowRoot;
} catch (error) {
if (!(error instanceof DOMException && error.name === 'SecurityError')) {
throw error;
}
return () => {
//
};
}
const originalPropertyDescriptor = patchTarget?.prototype
? Object.getOwnPropertyDescriptor(
patchTarget?.prototype,
Expand Down
16 changes: 14 additions & 2 deletions packages/rrweb/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -551,15 +551,27 @@ export function getRootShadowHost(n: Node): Node {
return rootShadowHost;
}

// In Firefox, reading `ownerDocument` on a node whose document has navigated
// cross-origin throws "Permission denied to access property 'ownerDocument'"
// (a SecurityError variant). Treat any such throw as "not in DOM" so the
// surrounding observer callback can continue.
function safeOwnerDocument(n: Node): Document | null {
try {
return n.ownerDocument;
} catch {
return null;
}
}

export function shadowHostInDom(n: Node): boolean {
const doc = n.ownerDocument;
const doc = safeOwnerDocument(n);
if (!doc) return false;
const shadowHost = getRootShadowHost(n);
return dom.contains(doc, shadowHost);
}

export function inDom(n: Node): boolean {
const doc = n.ownerDocument;
const doc = safeOwnerDocument(n);
if (!doc) return false;
return dom.contains(doc, n) || shadowHostInDom(n);
}
51 changes: 51 additions & 0 deletions packages/rrweb/test/record/observer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* @vitest-environment jsdom
*/
import { describe, it, expect, vi } from 'vitest';
import { Mirror } from '@posthog/rrweb-snapshot';

import { initAdoptedStyleSheetObserver } from '../../src/record/observer';

describe('initAdoptedStyleSheetObserver', () => {
it('does not throw when host.ownerDocument access throws SecurityError', () => {
// Reproduces the Firefox "Permission denied to access property
// 'ownerDocument'" thrown when a same-origin shadow host's owning
// document later navigates cross-origin. The observer must bail out
// cleanly instead of re-throwing and aborting the recorder.
const mirror = new Mirror();
const stylesheetManager = { adoptStyleSheets: vi.fn() } as any;

const hostEl = document.createElement('div');
document.body.appendChild(hostEl);
const shadowRoot = hostEl.attachShadow({ mode: 'open' });
mirror.add(hostEl, {
type: 2,
tagName: 'div',
attributes: {},
childNodes: [],
id: 1,
});
Object.defineProperty(shadowRoot, 'ownerDocument', {
get() {
throw new DOMException(
"Permission denied to access property 'ownerDocument'",
'SecurityError',
);
},
});

let cleanup: () => void = () => {
//
};
expect(() => {
cleanup = initAdoptedStyleSheetObserver(
{ mirror, stylesheetManager },
shadowRoot,
);
}).not.toThrow();
expect(typeof cleanup).toBe('function');
expect(() => cleanup()).not.toThrow();

document.body.removeChild(hostEl);
});
});
21 changes: 21 additions & 0 deletions packages/rrweb/test/util.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,5 +142,26 @@ describe('Utilities for other modules', () => {
expect(shadowHostInDom(a.childNodes[0])).toBeTruthy();
expect(inDom(a.childNodes[0])).toBeTruthy();
});

it('should not throw when ownerDocument access throws SecurityError', () => {
// Reproduces the Firefox "Permission denied to access property
// 'ownerDocument'" thrown when a node's owning document has navigated
// cross-origin. inDom/shadowHostInDom must treat this as "not in DOM"
// rather than re-throwing and aborting the surrounding observer.
const node = Object.create(Node.prototype) as Node;
Object.defineProperty(node, 'ownerDocument', {
get() {
throw new DOMException(
"Permission denied to access property 'ownerDocument'",
'SecurityError',
);
},
});

expect(() => inDom(node)).not.toThrow();
expect(inDom(node)).toBeFalsy();
expect(() => shadowHostInDom(node)).not.toThrow();
expect(shadowHostInDom(node)).toBeFalsy();
});
});
});
Loading