From 670fb3740ac8a3021c0ed1f79bf508d44721b83c Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Sat, 18 Jan 2025 11:34:16 +0000 Subject: [PATCH 1/7] fix: untainted prototype access from iframe --- packages/utils/src/index.ts | 47 ++++++++++++++++++++----------------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 1cd267c08f..1c85f1c2e3 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -80,29 +80,34 @@ export function getUntaintedPrototype( ), ); - if (isUntaintedAccessors && isUntaintedMethods && !isAngularZonePresent()) { - untaintedBasePrototype[key] = defaultObj.prototype as BasePrototypeCache[T]; - return defaultObj.prototype as BasePrototypeCache[T]; + const isUntainted = isUntaintedAccessors && isUntaintedMethods && !isAngularZonePresent(); + // we're going to default to what we do have + let impl: BasePrototypeCache[T] = defaultObj.prototype as BasePrototypeCache[T] + // but if it is tainted + if (!isUntainted) { + // try to load a fresh copy from a sandbox iframe + let iframeEl: HTMLIFrameElement | undefined = undefined; + try { + iframeEl = document.createElement('iframe'); + iframeEl.hidden = true; + document.head.appendChild(iframeEl); + const win = iframeEl.contentWindow; + if (win) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any + const candidate = (win as any)[key].prototype as BasePrototypeCache[T]; + if (candidate) { + impl = candidate + } + } + } finally { + if (iframeEl) { + document.head.removeChild(iframeEl); + } + } } - try { - const iframeEl = document.createElement('iframe'); - document.body.appendChild(iframeEl); - const win = iframeEl.contentWindow; - if (!win) return defaultObj.prototype as BasePrototypeCache[T]; - - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any - const untaintedObject = (win as any)[key] - .prototype as BasePrototypeCache[T]; - // cleanup - document.body.removeChild(iframeEl); - - if (!untaintedObject) return defaultPrototype; - - return (untaintedBasePrototype[key] = untaintedObject); - } catch { - return defaultPrototype; - } + untaintedBasePrototype[key] = impl; + return impl } const untaintedAccessorCache: Record< From 05cccbccbd2875109edf06fdeccb70b1a08de3ab Mon Sep 17 00:00:00 2001 From: pauldambra Date: Sat, 18 Jan 2025 11:42:24 +0000 Subject: [PATCH 2/7] Apply formatting changes --- packages/utils/src/index.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 1c85f1c2e3..4ed20cec9d 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -80,9 +80,11 @@ export function getUntaintedPrototype( ), ); - const isUntainted = isUntaintedAccessors && isUntaintedMethods && !isAngularZonePresent(); + const isUntainted = + isUntaintedAccessors && isUntaintedMethods && !isAngularZonePresent(); // we're going to default to what we do have - let impl: BasePrototypeCache[T] = defaultObj.prototype as BasePrototypeCache[T] + let impl: BasePrototypeCache[T] = + defaultObj.prototype as BasePrototypeCache[T]; // but if it is tainted if (!isUntainted) { // try to load a fresh copy from a sandbox iframe @@ -96,7 +98,7 @@ export function getUntaintedPrototype( // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any const candidate = (win as any)[key].prototype as BasePrototypeCache[T]; if (candidate) { - impl = candidate + impl = candidate; } } } finally { @@ -107,7 +109,7 @@ export function getUntaintedPrototype( } untaintedBasePrototype[key] = impl; - return impl + return impl; } const untaintedAccessorCache: Record< From 6b42a72d351034641c8d9e19e444e63e7a45c4a7 Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Sat, 18 Jan 2025 11:45:25 +0000 Subject: [PATCH 3/7] add changeset --- .changeset/tall-poems-develop.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/tall-poems-develop.md diff --git a/.changeset/tall-poems-develop.md b/.changeset/tall-poems-develop.md new file mode 100644 index 0000000000..975add5656 --- /dev/null +++ b/.changeset/tall-poems-develop.md @@ -0,0 +1,5 @@ +--- +"@rrweb/utils": patch +--- + +safer access to iframes in safari for untainted prototypes From a847c05d8e9bb92bd9a14829e28e63591d3dfb1a Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Thu, 30 Jan 2025 12:42:35 +0000 Subject: [PATCH 4/7] take native function off zone --- packages/utils/src/index.ts | 53 ++++++++++++++++++++++++------------- 1 file changed, 34 insertions(+), 19 deletions(-) diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 4ed20cec9d..c8f465a74c 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -28,30 +28,45 @@ const testableMethods = { const untaintedBasePrototype: Partial = {}; -/* - When angular patches things - particularly the MutationObserver - - they pass the `isNativeFunction` check - That then causes performance issues - because Angular's change detection - doesn't like sharing a mutation observer - Checking for the presence of the Zone object - on global is a good-enough proxy for Angular - to cover most cases - (you can configure zone.js to have a different name - on the global object and should then manually run rrweb - outside the Zone) - */ -export const isAngularZonePresent = (): boolean => { - return !!(globalThis as { Zone?: unknown }).Zone; +type WindowWithZone = typeof globalThis & { + Zone?: { + __symbol__?: (key: string) => string; + }; }; +type WindowWithUnpatchedSymbols = typeof globalThis & + Record; + +/* +Angular zone patches many things and can pass the untainted checks below, causing performance issues +Angular zone, puts the unpatched originals on the window, and the names for hose on the zone object. +So, we get the unpatched versions from the window object if they exist. +You can rename Zone, but this is a good enough proxy to avoid going to an iframe to get the untainted versions. +see: https://github.com/angular/angular/issues/26948 +*/ +function angularZoneUnpatchedAlternative(key: keyof BasePrototypeCache) { + const angularUnpatchedVersionSymbol = ( + globalThis as WindowWithZone + )?.Zone?.__symbol__?.(key); + if ( + angularUnpatchedVersionSymbol && + (globalThis as WindowWithUnpatchedSymbols)[angularUnpatchedVersionSymbol] + ) { + return (globalThis as WindowWithUnpatchedSymbols)[ + angularUnpatchedVersionSymbol + ]; + } else { + return undefined; + } +} + export function getUntaintedPrototype( key: T, ): BasePrototypeCache[T] { if (untaintedBasePrototype[key]) return untaintedBasePrototype[key] as BasePrototypeCache[T]; - const defaultObj = globalThis[key] as TypeofPrototypeOwner; + const defaultObj = angularZoneUnpatchedAlternative(key) || globalThis[key] as TypeofPrototypeOwner; const defaultPrototype = defaultObj.prototype as BasePrototypeCache[T]; // use list of testable accessors to check if the prototype is tainted @@ -81,7 +96,7 @@ export function getUntaintedPrototype( ); const isUntainted = - isUntaintedAccessors && isUntaintedMethods && !isAngularZonePresent(); + isUntaintedAccessors && isUntaintedMethods; // we're going to default to what we do have let impl: BasePrototypeCache[T] = defaultObj.prototype as BasePrototypeCache[T]; @@ -92,7 +107,7 @@ export function getUntaintedPrototype( try { iframeEl = document.createElement('iframe'); iframeEl.hidden = true; - document.head.appendChild(iframeEl); + document.body.appendChild(iframeEl); const win = iframeEl.contentWindow; if (win) { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any @@ -103,7 +118,7 @@ export function getUntaintedPrototype( } } finally { if (iframeEl) { - document.head.removeChild(iframeEl); + document.body.removeChild(iframeEl); } } } From fce17ef699cfcb8308fa6884e77b8252d89350d9 Mon Sep 17 00:00:00 2001 From: pauldambra Date: Thu, 30 Jan 2025 12:48:17 +0000 Subject: [PATCH 5/7] Apply formatting changes --- packages/utils/src/index.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index c8f465a74c..258a5b5d66 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -66,7 +66,9 @@ export function getUntaintedPrototype( if (untaintedBasePrototype[key]) return untaintedBasePrototype[key] as BasePrototypeCache[T]; - const defaultObj = angularZoneUnpatchedAlternative(key) || globalThis[key] as TypeofPrototypeOwner; + const defaultObj = + angularZoneUnpatchedAlternative(key) || + (globalThis[key] as TypeofPrototypeOwner); const defaultPrototype = defaultObj.prototype as BasePrototypeCache[T]; // use list of testable accessors to check if the prototype is tainted @@ -95,8 +97,7 @@ export function getUntaintedPrototype( ), ); - const isUntainted = - isUntaintedAccessors && isUntaintedMethods; + const isUntainted = isUntaintedAccessors && isUntaintedMethods; // we're going to default to what we do have let impl: BasePrototypeCache[T] = defaultObj.prototype as BasePrototypeCache[T]; From 1a28886ad1272f435b98031b0df11e976e433d48 Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Thu, 30 Jan 2025 12:50:45 +0000 Subject: [PATCH 6/7] Update .changeset/tall-poems-develop.md --- .changeset/tall-poems-develop.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/tall-poems-develop.md b/.changeset/tall-poems-develop.md index 975add5656..3324126c36 100644 --- a/.changeset/tall-poems-develop.md +++ b/.changeset/tall-poems-develop.md @@ -2,4 +2,4 @@ "@rrweb/utils": patch --- -safer access to iframes in safari for untainted prototypes +load unpatched versions of things from Angular zone when present From 7facb49a5408e4d935769af56432b5598f4e2e16 Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Wed, 5 Feb 2025 19:13:22 +0000 Subject: [PATCH 7/7] download v4 --- .github/workflows/style-check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/style-check.yml b/.github/workflows/style-check.yml index e47ee3c9c2..9aaf14a6e9 100644 --- a/.github/workflows/style-check.yml +++ b/.github/workflows/style-check.yml @@ -40,7 +40,7 @@ jobs: runs-on: ubuntu-latest name: ESLint Annotation steps: - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4 with: name: eslint_report.json - name: Annotate Code Linting Results