Skip to content

Commit 3371fb9

Browse files
authored
chore: do not leak pw internals (#35721)
1 parent 400f466 commit 3371fb9

File tree

10 files changed

+106
-69
lines changed

10 files changed

+106
-69
lines changed

packages/injected/src/injectedScript.ts

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import type { LayoutSelectorName } from './layoutSelectorUtils';
4444
import type { SelectorEngine, SelectorRoot } from './selectorEngine';
4545
import type { GenerateSelectorOptions } from './selectorGenerator';
4646
import type { ElementText, TextMatcher } from './selectorUtils';
47+
import type { Builtins } from '@isomorphic/builtins';
4748

4849

4950
export type FrameExpectParams = Omit<channels.FrameExpectParams, 'expectedValue'> & { expectedValue?: any };
@@ -64,6 +65,18 @@ interface WebKitLegacyDeviceMotionEvent extends DeviceMotionEvent {
6465
readonly initDeviceMotionEvent: (type: string, bubbles: boolean, cancelable: boolean, acceleration: DeviceMotionEventAcceleration, accelerationIncludingGravity: DeviceMotionEventAcceleration, rotationRate: DeviceMotionEventRotationRate, interval: number) => void;
6566
}
6667

68+
export type InjectedScriptOptions = {
69+
isUnderTest: boolean;
70+
sdkLanguage: Language;
71+
// For strict error and codegen
72+
testIdAttributeName: string;
73+
stableRafCount: number;
74+
browserName: string;
75+
inputFileRoleTextbox: boolean;
76+
customEngines: { name: string, source: string }[];
77+
runtimeGuid: string;
78+
};
79+
6780
export class InjectedScript {
6881
private _engines: Map<string, SelectorEngine>;
6982
readonly _evaluator: SelectorEvaluatorImpl;
@@ -96,7 +109,7 @@ export class InjectedScript {
96109
isInsideScope,
97110
normalizeWhiteSpace,
98111
parseAriaSnapshot,
99-
builtins: builtins(),
112+
builtins: null as unknown as Builtins,
100113
};
101114

102115
private _autoClosingTags: Set<string>;
@@ -108,15 +121,15 @@ export class InjectedScript {
108121
private _allHitTargetInterceptorEvents: Set<string>;
109122

110123
// eslint-disable-next-line no-restricted-globals
111-
constructor(window: Window & typeof globalThis, isUnderTest: boolean, sdkLanguage: Language, testIdAttributeNameForStrictErrorAndConsoleCodegen: string, stableRafCount: number, browserName: string, inputFileRoleTextbox: boolean, customEngines: { name: string, engine: SelectorEngine }[]) {
124+
constructor(window: Window & typeof globalThis, options: InjectedScriptOptions) {
112125
this.window = window;
113126
this.document = window.document;
114-
this.isUnderTest = isUnderTest;
127+
this.isUnderTest = options.isUnderTest;
115128
// Make sure builtins are created from "window". This is important for InjectedScript instantiated
116129
// inside a trace viewer snapshot, where "window" differs from "globalThis".
117-
this.utils.builtins = builtins(window);
118-
this._sdkLanguage = sdkLanguage;
119-
this._testIdAttributeNameForStrictErrorAndConsoleCodegen = testIdAttributeNameForStrictErrorAndConsoleCodegen;
130+
this.utils.builtins = builtins(options.runtimeGuid, window);
131+
this._sdkLanguage = options.sdkLanguage;
132+
this._testIdAttributeNameForStrictErrorAndConsoleCodegen = options.testIdAttributeName;
120133
this._evaluator = new SelectorEvaluatorImpl();
121134
this.consoleApi = new ConsoleAPI(this);
122135

@@ -216,17 +229,17 @@ export class InjectedScript {
216229
this._engines.set('internal:role', createRoleEngine(true));
217230
this._engines.set('aria-ref', this._createAriaIdEngine());
218231

219-
for (const { name, engine } of customEngines)
220-
this._engines.set(name, engine);
232+
for (const { name, source } of options.customEngines)
233+
this._engines.set(name, this.eval(source));
221234

222-
this._stableRafCount = stableRafCount;
223-
this._browserName = browserName;
224-
setGlobalOptions({ browserNameForWorkarounds: browserName, inputFileRoleTextbox });
235+
this._stableRafCount = options.stableRafCount;
236+
this._browserName = options.browserName;
237+
setGlobalOptions({ browserNameForWorkarounds: options.browserName, inputFileRoleTextbox: options.inputFileRoleTextbox });
225238

226239
this._setupGlobalListenersRemovalDetection();
227240
this._setupHitTargetInterceptors();
228241

229-
if (isUnderTest)
242+
if (options.isUnderTest)
230243
(this.window as any).__injectedScript = this;
231244
}
232245

packages/injected/src/utilityScript.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,19 @@
1717
import { builtins } from '@isomorphic/builtins';
1818
import { source } from '@isomorphic/utilityScriptSerializers';
1919

20+
import type { Builtins } from '@isomorphic/builtins';
21+
2022
export class UtilityScript {
21-
constructor(isUnderTest: boolean) {
23+
24+
private _builtins: Builtins;
25+
26+
constructor(runtimeGuid: string, isUnderTest: boolean) {
2227
if (isUnderTest) {
2328
// eslint-disable-next-line no-restricted-globals
24-
(globalThis as any).builtins = builtins();
29+
(globalThis as any).builtins = builtins(runtimeGuid);
2530
}
26-
const result = source(builtins());
31+
this._builtins = builtins(runtimeGuid);
32+
const result = source(this._builtins);
2733
this.serializeAsCallArgument = result.serializeAsCallArgument;
2834
this.parseEvaluationResultValue = result.parseEvaluationResultValue;
2935
}
@@ -38,7 +44,7 @@ export class UtilityScript {
3844
for (let i = 0; i < args.length; i++)
3945
parameters[i] = this.parseEvaluationResultValue(args[i], handles);
4046

41-
let result = builtins().eval(expression);
47+
let result = this._builtins.eval(expression);
4248
if (isFunction === true) {
4349
result = result(...parameters);
4450
} else if (isFunction === false) {

packages/playwright-core/src/server/bidi/bidiPage.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { eventsHelper } from '../utils/eventsHelper';
1919
import { BrowserContext } from '../browserContext';
2020
import * as dialog from '../dialog';
2121
import * as dom from '../dom';
22-
import { Page } from '../page';
22+
import { Page, PageBinding } from '../page';
2323
import { BidiExecutionContext, createHandle } from './bidiExecutionContext';
2424
import { RawKeyboardImpl, RawMouseImpl, RawTouchscreenImpl } from './bidiInput';
2525
import { BidiNetworkManager } from './bidiNetworkManager';
@@ -573,7 +573,7 @@ export class BidiPage implements PageDelegate {
573573
}
574574

575575
export function addMainBinding(callback: (arg: any) => void) {
576-
(globalThis as any)['__playwright__binding__'] = callback;
576+
(globalThis as any)[PageBinding.kPlaywrightBinding] = callback;
577577
}
578578

579579
function toBidiExecutionContext(executionContext: dom.FrameExecutionContext): BidiExecutionContext {

packages/playwright-core/src/server/browserContext.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import { mkdirIfNeeded } from './utils/fileUtils';
2828
import { HarRecorder } from './har/harRecorder';
2929
import { helper } from './helper';
3030
import { SdkObject, serverSideCallMetadata } from './instrumentation';
31-
import { builtins } from '../utils/isomorphic/builtins';
31+
import { builtinsSource } from '../utils/isomorphic/builtins';
3232
import * as utilityScriptSerializers from '../utils/isomorphic/utilityScriptSerializers';
3333
import * as network from './network';
3434
import { InitScript } from './page';
@@ -37,6 +37,7 @@ import { Recorder } from './recorder';
3737
import { RecorderApp } from './recorder/recorderApp';
3838
import * as storageScript from './storageScript';
3939
import { Tracing } from './trace/recorder/tracing';
40+
import * as js from './javascript';
4041

4142
import type { Artifact } from './artifact';
4243
import type { Browser, BrowserOptions } from './browser';
@@ -518,7 +519,7 @@ export abstract class BrowserContext extends SdkObject {
518519
};
519520
const originsToSave = new Set(this._origins);
520521

521-
const collectScript = `(${storageScript.collect})(${utilityScriptSerializers.source}, (${builtins})(), ${this._browser.options.name === 'firefox'}, ${indexedDB})`;
522+
const collectScript = `(${storageScript.collect})(${utilityScriptSerializers.source}, ${builtinsSource(js.runtimeGuid)}, ${this._browser.options.name === 'firefox'}, ${indexedDB})`;
522523

523524
// First try collecting storage stage from existing pages.
524525
for (const page of this.pages()) {
@@ -611,7 +612,7 @@ export abstract class BrowserContext extends SdkObject {
611612
for (const originState of state.origins) {
612613
const frame = page.mainFrame();
613614
await frame.goto(metadata, originState.origin);
614-
await frame.evaluateExpression(`(${storageScript.restore})(${utilityScriptSerializers.source}, (${builtins})(), ${JSON.stringify(originState)})`, { world: 'utility' });
615+
await frame.evaluateExpression(`(${storageScript.restore})(${utilityScriptSerializers.source}, ${builtinsSource(js.runtimeGuid)}, ${JSON.stringify(originState)})`, { world: 'utility' });
615616
}
616617
await page.close(internalMetadata);
617618
}

packages/playwright-core/src/server/dom.ts

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import { isSessionClosedError } from './protocolError';
2424
import * as injectedScriptSource from '../generated/injectedScriptSource';
2525

2626
import type * as frames from './frames';
27-
import type { ElementState, HitTargetInterceptionResult, InjectedScript } from '@injected/injectedScript';
27+
import type { ElementState, HitTargetInterceptionResult, InjectedScript, InjectedScriptOptions } from '@injected/injectedScript';
2828
import type { CallMetadata } from './instrumentation';
2929
import type { Page } from './page';
3030
import type { Progress } from './progress';
@@ -85,25 +85,26 @@ export class FrameExecutionContext extends js.ExecutionContext {
8585

8686
injectedScript(): Promise<js.JSHandle<InjectedScript>> {
8787
if (!this._injectedScriptPromise) {
88-
const custom: string[] = [];
88+
const customEngines: InjectedScriptOptions['customEngines'] = [];
8989
const selectorsRegistry = this.frame._page.context().selectors();
9090
for (const [name, { source }] of selectorsRegistry._engines)
91-
custom.push(`{ name: '${name}', engine: (${source}) }`);
91+
customEngines.push({ name, source });
9292
const sdkLanguage = this.frame.attribution.playwright.options.sdkLanguage;
93+
const options: InjectedScriptOptions = {
94+
isUnderTest: isUnderTest(),
95+
sdkLanguage,
96+
testIdAttributeName: selectorsRegistry.testIdAttributeName(),
97+
stableRafCount: this.frame._page._delegate.rafCountForStablePosition(),
98+
browserName: this.frame._page._browserContext._browser.options.name,
99+
inputFileRoleTextbox: process.env.PLAYWRIGHT_INPUT_FILE_TEXTBOX ? true : false,
100+
customEngines,
101+
runtimeGuid: js.runtimeGuid,
102+
};
93103
const source = `
94104
(() => {
95105
const module = {};
96106
${injectedScriptSource.source}
97-
return new (module.exports.InjectedScript())(
98-
globalThis,
99-
${isUnderTest()},
100-
"${sdkLanguage}",
101-
${JSON.stringify(selectorsRegistry.testIdAttributeName())},
102-
${this.frame._page._delegate.rafCountForStablePosition()},
103-
"${this.frame._page._browserContext._browser.options.name}",
104-
${process.env.PLAYWRIGHT_INPUT_FILE_TEXTBOX ? 'true' : 'false'},
105-
[${custom.join(',\n')}]
106-
);
107+
return new (module.exports.InjectedScript())(globalThis, ${JSON.stringify(options)});
107108
})();
108109
`;
109110
this._injectedScriptPromise = this.rawEvaluateHandle(source)

packages/playwright-core/src/server/javascript.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,17 @@
1616

1717
import { SdkObject } from './instrumentation';
1818
import * as utilityScriptSource from '../generated/utilityScriptSource';
19-
import { isUnderTest } from '../utils';
19+
import { createGuid, isUnderTest } from '../utils';
2020
import { builtins } from '../utils/isomorphic/builtins';
2121
import { source } from '../utils/isomorphic/utilityScriptSerializers';
2222
import { LongStandingScope } from '../utils/isomorphic/manualPromise';
2323

2424
import type * as dom from './dom';
2525
import type { UtilityScript } from '@injected/utilityScript';
2626

27+
// Use in the web-facing names to avoid leaking Playwright to the pages.
28+
export const runtimeGuid = createGuid();
29+
2730
interface TaggedAsJSHandle<T> {
2831
__jshandle: T;
2932
}
@@ -46,7 +49,7 @@ export type Func1<Arg, R> = string | ((arg: Unboxed<Arg>) => R | Promise<R>);
4649
export type FuncOn<On, Arg2, R> = string | ((on: On, arg2: Unboxed<Arg2>) => R | Promise<R>);
4750
export type SmartHandle<T> = T extends Node ? dom.ElementHandle<T> : JSHandle<T>;
4851

49-
const utilityScriptSerializers = source(builtins());
52+
const utilityScriptSerializers = source(builtins(runtimeGuid));
5053
export const parseEvaluationResultValue = utilityScriptSerializers.parseEvaluationResultValue;
5154
export const serializeAsCallArgument = utilityScriptSerializers.serializeAsCallArgument;
5255

@@ -109,7 +112,7 @@ export class ExecutionContext extends SdkObject {
109112
(() => {
110113
const module = {};
111114
${utilityScriptSource.source}
112-
return new (module.exports.UtilityScript())(${isUnderTest()});
115+
return new (module.exports.UtilityScript())(${JSON.stringify(runtimeGuid)}, ${isUnderTest()});
113116
})();`;
114117
this._utilityScriptPromise = this._raceAgainstContextDestroyed(this.delegate.rawEvaluateHandle(this, source))
115118
.then(handle => {

packages/playwright-core/src/server/page.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import * as frames from './frames';
2424
import { helper } from './helper';
2525
import * as input from './input';
2626
import { SdkObject } from './instrumentation';
27-
import { builtins } from '../utils/isomorphic/builtins';
27+
import { builtinsSource } from '../utils/isomorphic/builtins';
2828
import { createPageBindingScript, deliverBindingResult, takeBindingHandle } from './pageBinding';
2929
import * as js from './javascript';
3030
import { ProgressController } from './progress';
@@ -858,7 +858,7 @@ export class Worker extends SdkObject {
858858
}
859859

860860
export class PageBinding {
861-
static kPlaywrightBinding = '__playwright__binding__';
861+
static kPlaywrightBinding = '__playwright__binding__' + js.runtimeGuid;
862862

863863
readonly name: string;
864864
readonly playwrightFunction: frames.FunctionWithSource;
@@ -906,19 +906,23 @@ export class InitScript {
906906
constructor(source: string, internal?: boolean, name?: string) {
907907
const guid = createGuid();
908908
this.source = `(() => {
909-
globalThis.__pwInitScripts = globalThis.__pwInitScripts || {};
910-
const hasInitScript = globalThis.__pwInitScripts[${JSON.stringify(guid)}];
909+
const name = '__pw_init_scripts__${js.runtimeGuid}';
910+
if (!globalThis[name])
911+
Object.defineProperty(globalThis, name, { value: {}, configurable: false, enumerable: false, writable: false });
912+
913+
const globalInitScripts = globalThis[name];
914+
const hasInitScript = globalInitScripts[${JSON.stringify(guid)}];
911915
if (hasInitScript)
912916
return;
913-
globalThis.__pwInitScripts[${JSON.stringify(guid)}] = true;
917+
globalThis[name][${JSON.stringify(guid)}] = true;
914918
${source}
915919
})();`;
916920
this.internal = !!internal;
917921
this.name = name;
918922
}
919923
}
920924

921-
export const kBuiltinsScript = new InitScript(`(${builtins})()`, true /* internal */);
925+
export const kBuiltinsScript = new InitScript(builtinsSource(js.runtimeGuid), true /* internal */);
922926

923927
class FrameThrottler {
924928
private _acks: (() => void)[] = [];

packages/playwright-core/src/server/pageBinding.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { builtins } from '../utils/isomorphic/builtins';
17+
import { builtinsSource } from '../utils/isomorphic/builtins';
1818
import { source } from '../utils/isomorphic/utilityScriptSerializers';
19+
import * as js from './javascript';
1920

2021
import type { Builtins } from '../utils/isomorphic/builtins';
2122
import type { SerializedValue } from '../utils/isomorphic/utilityScriptSerializers';
@@ -88,5 +89,5 @@ export function deliverBindingResult(arg: { name: string, seq: number, result?:
8889
}
8990

9091
export function createPageBindingScript(playwrightBinding: string, name: string, needsHandle: boolean) {
91-
return `(${addPageBinding.toString()})(${JSON.stringify(playwrightBinding)}, ${JSON.stringify(name)}, ${needsHandle}, (${source}), (${builtins})())`;
92+
return `(${addPageBinding.toString()})(${JSON.stringify(playwrightBinding)}, ${JSON.stringify(name)}, ${needsHandle}, (${source}), ${builtinsSource(js.runtimeGuid)})`;
9293
}

packages/playwright-core/src/utils/isomorphic/builtins.ts

Lines changed: 30 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -41,31 +41,39 @@ export type Builtins = {
4141
// anything else happens in the page. This way, original builtins are saved on the global object
4242
// before page can temper with them. Later on, any call to builtins() will retrieve the stored
4343
// builtins instead of initializing them again.
44-
export function builtins(global?: typeof globalThis): Builtins {
44+
export function builtins(runtimeGuid: string, global?: typeof globalThis): Builtins {
4545
global = global ?? globalThis;
46-
if (!(global as any)['__playwright_builtins__']) {
47-
const builtins: Builtins = {
48-
setTimeout: global.setTimeout?.bind(global),
49-
clearTimeout: global.clearTimeout?.bind(global),
50-
setInterval: global.setInterval?.bind(global),
51-
clearInterval: global.clearInterval?.bind(global),
52-
requestAnimationFrame: global.requestAnimationFrame?.bind(global),
53-
cancelAnimationFrame: global.cancelAnimationFrame?.bind(global),
54-
requestIdleCallback: global.requestIdleCallback?.bind(global),
55-
cancelIdleCallback: global.cancelIdleCallback?.bind(global),
56-
performance: global.performance,
57-
eval: global.eval?.bind(global),
58-
Intl: global.Intl,
59-
Date: global.Date,
60-
Map: global.Map,
61-
Set: global.Set,
62-
};
63-
Object.defineProperty(global, '__playwright_builtins__', { value: builtins, configurable: false, enumerable: false, writable: false });
64-
}
65-
return (global as any)['__playwright_builtins__'];
46+
const name = `__playwright_builtins__${runtimeGuid}`;
47+
let builtins: Builtins = (global as any)[name];
48+
if (builtins)
49+
return builtins;
50+
51+
builtins = {
52+
setTimeout: global.setTimeout?.bind(global),
53+
clearTimeout: global.clearTimeout?.bind(global),
54+
setInterval: global.setInterval?.bind(global),
55+
clearInterval: global.clearInterval?.bind(global),
56+
requestAnimationFrame: global.requestAnimationFrame?.bind(global),
57+
cancelAnimationFrame: global.cancelAnimationFrame?.bind(global),
58+
requestIdleCallback: global.requestIdleCallback?.bind(global),
59+
cancelIdleCallback: global.cancelIdleCallback?.bind(global),
60+
performance: global.performance,
61+
eval: global.eval?.bind(global),
62+
Intl: global.Intl,
63+
Date: global.Date,
64+
Map: global.Map,
65+
Set: global.Set,
66+
};
67+
if (runtimeGuid)
68+
Object.defineProperty(global, name, { value: builtins, configurable: false, enumerable: false, writable: false });
69+
return builtins;
70+
}
71+
72+
export function builtinsSource(runtimeGuid: string): string {
73+
return `(${builtins})(${JSON.stringify(runtimeGuid)})`;
6674
}
6775

68-
const instance = builtins();
76+
const instance = builtins('');
6977
export const setTimeout = instance.setTimeout;
7078
export const clearTimeout = instance.clearTimeout;
7179
export const setInterval = instance.setInterval;

packages/trace-viewer/src/ui/snapshotTab.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ export const SnapshotTabsView: React.FunctionComponent<{
7777
<ToolbarButton icon='link-external' title='Open snapshot in a new tab' disabled={!snapshotUrls?.popoutUrl} onClick={() => {
7878
const win = window.open(snapshotUrls?.popoutUrl || '', '_blank');
7979
win?.addEventListener('DOMContentLoaded', () => {
80-
const injectedScript = new InjectedScript(win as any, false, sdkLanguage, testIdAttributeName, 1, 'chromium', false, []);
80+
const injectedScript = new InjectedScript(win as any, { isUnderTest: false, sdkLanguage, testIdAttributeName, stableRafCount: 1, browserName: 'chromium', inputFileRoleTextbox: false, customEngines: [], runtimeGuid: '' });
8181
injectedScript.consoleApi.install();
8282
});
8383
}} />
@@ -280,7 +280,7 @@ function createRecorders(recorders: { recorder: Recorder, frameSelector: string
280280
return;
281281
const win = frameWindow as any;
282282
if (!win._recorder) {
283-
const injectedScript = new InjectedScript(frameWindow as any, isUnderTest, sdkLanguage, testIdAttributeName, 1, 'chromium', false, []);
283+
const injectedScript = new InjectedScript(frameWindow as any, { isUnderTest, sdkLanguage, testIdAttributeName, stableRafCount: 1, browserName: 'chromium', inputFileRoleTextbox: false, customEngines: [], runtimeGuid: '' });
284284
const recorder = new Recorder(injectedScript);
285285
win._injectedScript = injectedScript;
286286
win._recorder = { recorder, frameSelector: parentFrameSelector };

0 commit comments

Comments
 (0)