Skip to content

Commit 5aef34c

Browse files
authored
fix: prevent iframe leak in untainted prototype and avoid unnecessary iframe creation (#159)
## Summary - Adopts upstream rrweb [#1770](rrweb-io/rrweb#1770) and [#1802](rrweb-io/rrweb#1802) - **#1770**: Wraps untainted prototype iframe creation in `try/finally` so the iframe is always removed, even on early return (when `contentWindow` is null) or exception. Previously these iframes would leak into the DOM. - **#1802**: Moves `querySelector`/`querySelectorAll` from `testableAccessors` to `testableMethods` and switches helpers from `getUntaintedAccessor` to `getUntaintedMethod`. These are methods, not property accessors, so the accessor check (`getOwnPropertyDescriptor(...).get`) always failed, causing a throwaway iframe to be created every time just to get the untainted prototype. ## Why Both fixes are in `packages/utils/src/index.ts` and affect the same `getUntaintedPrototype` code path. #1770 prevents DOM pollution from leaked iframes. #1802 avoids unnecessary iframe creation on every querySelector/querySelectorAll call, which is a hot path during recording. ## Test plan - [ ] Verify no regressions in recording on pages with patched DOM prototypes (Angular apps) - [ ] Inspect DOM during recording to confirm no orphaned iframes from untainted prototype detection
1 parent 1939c91 commit 5aef34c

File tree

1 file changed

+9
-7
lines changed

1 file changed

+9
-7
lines changed

packages/utils/src/index.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,14 @@ type BasePrototypeCache = {
1515
const testableAccessors = {
1616
Node: ['childNodes', 'parentNode', 'parentElement', 'textContent'] as const,
1717
ShadowRoot: ['host', 'styleSheets'] as const,
18-
Element: ['shadowRoot', 'querySelector', 'querySelectorAll'] as const,
18+
Element: ['shadowRoot'] as const,
1919
MutationObserver: [] as const,
2020
} as const;
2121

2222
const testableMethods = {
2323
Node: ['contains', 'getRootNode'] as const,
2424
ShadowRoot: ['getSelection'],
25-
Element: [],
25+
Element: ['querySelector', 'querySelectorAll'],
2626
MutationObserver: ['constructor'],
2727
} as const;
2828

@@ -102,23 +102,25 @@ export function getUntaintedPrototype<T extends keyof BasePrototypeCache>(
102102
return candidate.prototype as BasePrototypeCache[T];
103103
}
104104

105+
const iframeEl = document.createElement('iframe');
105106
try {
106-
const iframeEl = document.createElement('iframe');
107107
document.body.appendChild(iframeEl);
108108
const win = iframeEl.contentWindow;
109109
if (!win) return candidate.prototype as BasePrototypeCache[T];
110110

111111
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
112112
const untaintedObject = (win as any)[key]
113113
.prototype as BasePrototypeCache[T];
114-
// cleanup
115-
document.body.removeChild(iframeEl);
116114

117115
if (!untaintedObject) return defaultPrototype;
118116

119117
return (untaintedBasePrototype[key] = untaintedObject);
120118
} catch {
121119
return defaultPrototype;
120+
} finally {
121+
if (iframeEl.parentNode) {
122+
document.body.removeChild(iframeEl);
123+
}
122124
}
123125
}
124126

@@ -225,14 +227,14 @@ export function shadowRoot(n: Node): ShadowRoot | null {
225227
}
226228

227229
export function querySelector(n: Element, selectors: string): Element | null {
228-
return getUntaintedAccessor('Element', n, 'querySelector')(selectors);
230+
return getUntaintedMethod('Element', n, 'querySelector')(selectors);
229231
}
230232

231233
export function querySelectorAll(
232234
n: Element,
233235
selectors: string,
234236
): NodeListOf<Element> {
235-
return getUntaintedAccessor('Element', n, 'querySelectorAll')(selectors);
237+
return getUntaintedMethod('Element', n, 'querySelectorAll')(selectors);
236238
}
237239

238240
export function mutationObserverCtor(): (typeof MutationObserver)['prototype']['constructor'] {

0 commit comments

Comments
 (0)