Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Catch calls to iframe content document #222

Merged
merged 3 commits into from
Nov 19, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
6 changes: 4 additions & 2 deletions packages/rrdom/src/diff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import type {
RRDocument,
Mirror,
} from '.';
import { getIFrameContentDocument } from './util';

const NAMESPACES: Record<string, string> = {
svg: 'http://www.w3.org/2000/svg',
Expand Down Expand Up @@ -170,8 +171,9 @@ function diffBeforeUpdatingChildren(
const newRRElement = newTree as IRRElement;
switch (newRRElement.tagName) {
case 'IFRAME': {
const oldContentDocument = (oldTree as HTMLIFrameElement)
.contentDocument;
const oldContentDocument = getIFrameContentDocument(
oldTree as HTMLIFrameElement,
);
// If the iframe is cross-origin, the contentDocument will be null.
if (!oldContentDocument) break;
// IFrame element doesn't have child nodes, so here we update its content document separately.
Expand Down
4 changes: 3 additions & 1 deletion packages/rrdom/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
IRRText,
IRRComment,
} from './document';
import { getIFrameContentDocument } from './util';

export class RRDocument extends BaseRRDocumentImpl(RRNode) {
private UNSERIALIZED_STARTING_ID = -2;
Expand Down Expand Up @@ -313,7 +314,7 @@ export function buildFromDom(
}

if (node.nodeName === 'IFRAME') {
const iframeDoc = (node as HTMLIFrameElement).contentDocument;
const iframeDoc = getIFrameContentDocument(node as HTMLIFrameElement);
iframeDoc && walk(iframeDoc, rrNode);
} else if (
node.nodeType === NodeType.DOCUMENT_NODE ||
Expand Down Expand Up @@ -485,3 +486,4 @@ export { RRNode };

export { diff, createOrGetNode, ReplayerHandler } from './diff';
export * from './document';
export { getIFrameContentDocument, getIFrameContentWindow } from './util';
27 changes: 27 additions & 0 deletions packages/rrdom/src/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* Get the content document of an iframe.
* Catching errors is necessary because some older browsers block access to the content document of a sandboxed iframe.
*/
export function getIFrameContentDocument(iframe?: HTMLIFrameElement) {
if (iframe) {
chargome marked this conversation as resolved.
Show resolved Hide resolved
try {
return iframe.contentDocument;
} catch (e) {
// noop
}
}
}

/**
* Get the content window of an iframe.
* Catching errors is necessary because some older browsers block access to the content document of a sandboxed iframe.
*/
export function getIFrameContentWindow(iframe?: HTMLIFrameElement) {
if (iframe) {
chargome marked this conversation as resolved.
Show resolved Hide resolved
try {
return iframe.contentWindow;
} catch (e) {
// noop
}
}
}
5 changes: 3 additions & 2 deletions packages/rrweb-snapshot/src/snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
shouldMaskInput,
setTimeout,
clearTimeout,
getIframeContentDocument,
} from './utils';

let _id = 1;
Expand Down Expand Up @@ -276,7 +277,7 @@
export function ignoreAttribute(
tagName: string,
name: string,
_value: unknown,

Check warning on line 280 in packages/rrweb-snapshot/src/snapshot.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb-snapshot/src/snapshot.ts#L280

[@typescript-eslint/no-unused-vars] '_value' is defined but never used.
): boolean {
return (tagName === 'video' || tagName === 'audio') && name === 'autoplay';
}
Expand Down Expand Up @@ -513,7 +514,7 @@
iframeEl.addEventListener('load', listener);
}

function isStylesheetLoaded(link: HTMLLinkElement) {

Check warning on line 517 in packages/rrweb-snapshot/src/snapshot.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb-snapshot/src/snapshot.ts#L517

[@typescript-eslint/no-unused-vars] 'isStylesheetLoaded' is defined but never used.
if (!link.getAttribute('href')) return true; // nothing to load
return link.sheet !== null;
}
Expand Down Expand Up @@ -722,7 +723,7 @@
// So we'll be conservative and keep textContent as-is.
} else if ((n.parentNode as HTMLStyleElement).sheet?.cssRules) {
textContent = stringifyStylesheet(
(n.parentNode as HTMLStyleElement).sheet!,

Check warning on line 726 in packages/rrweb-snapshot/src/snapshot.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb-snapshot/src/snapshot.ts#L726

[@typescript-eslint/no-non-null-assertion] Forbidden non-null assertion.
);
}
} catch (err) {
Expand Down Expand Up @@ -829,7 +830,7 @@
keepIframeSrcFn,
newlyAddedElement = false,
rootId,
maskAllText,

Check warning on line 833 in packages/rrweb-snapshot/src/snapshot.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb-snapshot/src/snapshot.ts#L833

[@typescript-eslint/no-unused-vars] 'maskAllText' is assigned a value but never used.
maskTextClass,
unmaskTextClass,
maskTextSelector,
Expand Down Expand Up @@ -871,7 +872,7 @@
if (cssText) {
delete attributes.rel;
delete attributes.href;
attributes._cssText = absoluteToStylesheet(cssText, stylesheet!.href!);

Check warning on line 875 in packages/rrweb-snapshot/src/snapshot.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb-snapshot/src/snapshot.ts#L875

[@typescript-eslint/no-non-null-assertion] Forbidden non-null assertion.

Check warning on line 875 in packages/rrweb-snapshot/src/snapshot.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb-snapshot/src/snapshot.ts#L875

[@typescript-eslint/no-non-null-assertion] Forbidden non-null assertion.
}
}
// dynamic stylesheet
Expand Down Expand Up @@ -983,10 +984,10 @@
const recordInlineImage = () => {
image.removeEventListener('load', recordInlineImage);
try {
canvasService!.width = image.naturalWidth;

Check warning on line 987 in packages/rrweb-snapshot/src/snapshot.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb-snapshot/src/snapshot.ts#L987

[@typescript-eslint/no-non-null-assertion] Forbidden non-null assertion.
canvasService!.height = image.naturalHeight;

Check warning on line 988 in packages/rrweb-snapshot/src/snapshot.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb-snapshot/src/snapshot.ts#L988

[@typescript-eslint/no-non-null-assertion] Forbidden non-null assertion.
canvasCtx!.drawImage(image, 0, 0);

Check warning on line 989 in packages/rrweb-snapshot/src/snapshot.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb-snapshot/src/snapshot.ts#L989

[@typescript-eslint/no-non-null-assertion] Forbidden non-null assertion.
attributes.rr_dataURL = canvasService!.toDataURL(

Check warning on line 990 in packages/rrweb-snapshot/src/snapshot.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb-snapshot/src/snapshot.ts#L990

[@typescript-eslint/no-non-null-assertion] Forbidden non-null assertion.
dataURLOptions.type,
dataURLOptions.quality,
);
Expand Down Expand Up @@ -1046,7 +1047,7 @@
if (tagName === 'iframe' && !keepIframeSrcFn(attributes.src as string)) {
// Don't try to access `contentDocument` if iframe is blocked, otherwise it
// will trigger browser warnings.
if (!needBlock && !(n as HTMLIFrameElement).contentDocument) {
if (!needBlock && !getIframeContentDocument(n as HTMLIFrameElement)) {
// we can't record it directly as we can't see into it
// preserve the src attribute so a decision can be taken at replay time
attributes.rr_src = attributes.src;
Expand Down Expand Up @@ -1384,7 +1385,7 @@
onceIframeLoaded(
n as HTMLIFrameElement,
() => {
const iframeDoc = (n as HTMLIFrameElement).contentDocument;
const iframeDoc = getIframeContentDocument(n as HTMLIFrameElement);
if (iframeDoc && onIframeLoad) {
const serializedIframeNode = serializeNodeWithId(iframeDoc, {
doc: iframeDoc,
Expand Down
14 changes: 14 additions & 0 deletions packages/rrweb-snapshot/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@
* Browsers sometimes incorrectly escape `@import` on `.cssText` statements.
* This function tries to correct the escaping.
* more info: https://bugs.chromium.org/p/chromium/issues/detail?id=1472259
* @param cssImportRule

Check warning on line 74 in packages/rrweb-snapshot/src/utils.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb-snapshot/src/utils.ts#L74

[tsdoc/syntax] tsdoc-param-tag-missing-hyphen: The @param block should be followed by a parameter name and then a hyphen
* @returns `cssText` with browser inconsistencies fixed, or null if not applicable.
*/
export function escapeImportStatement(rule: CSSImportRule): string {
Expand Down Expand Up @@ -454,3 +454,17 @@
): ReturnType<typeof window.clearTimeout> {
return getImplementation('clearTimeout')(...rest);
}

/**
* Get the content document of an iframe.
* Catching errors is necessary because some older browsers block access to the content document of a sandboxed iframe.
*/
export function getIframeContentDocument(iframe?: HTMLIFrameElement) {
try {
if (iframe) {
return iframe.contentDocument;
}
} catch (e) {
// noop
}
}
Copy link
Member Author

Choose a reason for hiding this comment

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

duplicated it here to not create a new dependency

13 changes: 8 additions & 5 deletions packages/rrweb/src/record/iframe-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type {
mutationCallBack,
} from '@sentry-internal/rrweb-types';
import type { StylesheetManager } from './stylesheet-manager';
import { getIFrameContentDocument } from '@sentry-internal/rrdom';

export interface IframeManagerInterface {
crossOriginIframeMirror: CrossOriginIframeMirror;
Expand Down Expand Up @@ -109,14 +110,16 @@ export class IframeManager implements IframeManagerInterface {
});
this.loadListener?.(iframeEl);

const iframeDoc = getIFrameContentDocument(iframeEl);

if (
iframeEl.contentDocument &&
iframeEl.contentDocument.adoptedStyleSheets &&
iframeEl.contentDocument.adoptedStyleSheets.length > 0
iframeDoc &&
iframeDoc.adoptedStyleSheets &&
iframeDoc.adoptedStyleSheets.length > 0
)
this.stylesheetManager.adoptStyleSheets(
iframeEl.contentDocument.adoptedStyleSheets,
this.mirror.getId(iframeEl.contentDocument),
iframeDoc.adoptedStyleSheets,
this.mirror.getId(iframeDoc),
);
}
private handleMessage(message: MessageEvent | CrossOriginIframeMessageEvent) {
Expand Down
6 changes: 5 additions & 1 deletion packages/rrweb/src/record/mutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
getShadowHost,
closestElementOfNode,
} from '../utils';
import { getIFrameContentDocument } from '@sentry-internal/rrdom';

type DoubleLinkedListNode = {
previous: DoubleLinkedListNode | null;
Expand Down Expand Up @@ -386,13 +387,13 @@
};

while (this.mapRemoves.length) {
this.mirror.removeNodeFromMap(this.mapRemoves.shift()!);

Check warning on line 390 in packages/rrweb/src/record/mutation.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb/src/record/mutation.ts#L390

[@typescript-eslint/no-non-null-assertion] Forbidden non-null assertion.
}

for (const n of this.movedSet) {
if (
isParentRemoved(this.removes, n, this.mirror) &&
!this.movedSet.has(n.parentNode!)

Check warning on line 396 in packages/rrweb/src/record/mutation.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb/src/record/mutation.ts#L396

[@typescript-eslint/no-non-null-assertion] Forbidden non-null assertion.
) {
continue;
}
Expand Down Expand Up @@ -628,7 +629,10 @@
attributeName === 'src' &&
!this.keepIframeSrcFn(value as string)
) {
if (!(target as HTMLIFrameElement).contentDocument) {
const iframeDoc = getIFrameContentDocument(
target as HTMLIFrameElement,
);
if (!iframeDoc) {
// we can't record it directly as we can't see into it
// preserve the src attribute so a decision can be taken at replay time
attributeName = 'rr_src';
Expand Down
13 changes: 9 additions & 4 deletions packages/rrweb/src/record/shadow-dom-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ import {
import { patch, inDom, setTimeout } from '../utils';
import type { Mirror } from '@sentry-internal/rrweb-snapshot';
import { isNativeShadowDom } from '@sentry-internal/rrweb-snapshot';
import {
getIFrameContentDocument,
getIFrameContentWindow,
} from '@sentry-internal/rrdom';

type BypassOptions = Omit<
MutationBufferParam,
Expand Down Expand Up @@ -122,15 +126,16 @@ export class ShadowDomManager implements ShadowDomManagerInterface {
* Monkey patch 'attachShadow' of an IFrameElement to observe newly added shadow doms.
*/
public observeAttachShadow(iframeElement: HTMLIFrameElement) {
if (!iframeElement.contentWindow || !iframeElement.contentDocument) return;

const iframeDoc = getIFrameContentDocument(iframeElement);
const iframeWindow = getIFrameContentWindow(iframeElement);
if (!iframeDoc || !iframeWindow) return;
this.patchAttachShadow(
(
iframeElement.contentWindow as Window & {
iframeWindow as Window & {
Element: { prototype: Element };
}
).Element,
iframeElement.contentDocument,
iframeDoc,
);
}

Expand Down
Loading
Loading