From 477586429d1fb8147110e142783075d980f00026 Mon Sep 17 00:00:00 2001 From: Blake Byrnes Date: Wed, 20 Mar 2024 12:24:42 -0400 Subject: [PATCH] =?UTF-8?q?fix(plugins):=20don=E2=80=99t=20scope=20emulato?= =?UTF-8?q?r=20vars=20in=20window?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix(plugins): toString of navigator exploding --- .../HTMLIFrameElement.prototype.ts | 17 +++--- .../injected-scripts/_proxyUtils.ts | 56 ++++++++++++----- .../injected-scripts/navigator.ts | 9 +++ .../lib/DomOverridesBuilder.ts | 6 +- .../test/detection.test.ts | 61 +++++++++++++++++++ yarn.lock | 10 --- 6 files changed, 124 insertions(+), 35 deletions(-) diff --git a/plugins/default-browser-emulator/injected-scripts/HTMLIFrameElement.prototype.ts b/plugins/default-browser-emulator/injected-scripts/HTMLIFrameElement.prototype.ts index da6271a73..e900e00f5 100644 --- a/plugins/default-browser-emulator/injected-scripts/HTMLIFrameElement.prototype.ts +++ b/plugins/default-browser-emulator/injected-scripts/HTMLIFrameElement.prototype.ts @@ -1,23 +1,24 @@ // This bypass is based on the one from puppeteer-stealth-evasions -declare let originalContentWindow; -declare let frameWindowProxies; -declare let hasRunNewDocumentScripts; +declare let scopedVars: any; -if (typeof frameWindowProxies === 'undefined') { - frameWindowProxies = new WeakMap(); - hasRunNewDocumentScripts = new WeakSet(); +if (typeof scopedVars.frameWindowProxies === 'undefined') { + scopedVars.frameWindowProxies = new WeakMap(); + scopedVars.hasRunNewDocumentScripts = new WeakSet(); - originalContentWindow = Object.getOwnPropertyDescriptor( + scopedVars.originalContentWindow = Object.getOwnPropertyDescriptor( self.HTMLIFrameElement.prototype, 'contentWindow', ).get; function getTrueContentWindow(frame: HTMLIFrameElement): Window { - return originalContentWindow.apply(frame); + return scopedVars.originalContentWindow.apply(frame); } } +const frameWindowProxies = scopedVars.frameWindowProxies; +const hasRunNewDocumentScripts = scopedVars.hasRunNewDocumentScripts; + proxyGetter(self.HTMLIFrameElement.prototype, 'contentWindow', (target, iframe) => { if (frameWindowProxies.has(iframe) && iframe.isConnected) { return frameWindowProxies.get(iframe); diff --git a/plugins/default-browser-emulator/injected-scripts/_proxyUtils.ts b/plugins/default-browser-emulator/injected-scripts/_proxyUtils.ts index 17603c787..287796b59 100644 --- a/plugins/default-browser-emulator/injected-scripts/_proxyUtils.ts +++ b/plugins/default-browser-emulator/injected-scripts/_proxyUtils.ts @@ -17,6 +17,8 @@ const ReflectCached = { getOwnPropertyDescriptor: Reflect.getOwnPropertyDescriptor.bind(Reflect), }; +const ErrorCached = Error; + const ObjectCached = { setPrototypeOf: Object.setPrototypeOf.bind(Object), getPrototypeOf: Object.getPrototypeOf.bind(Object), @@ -56,14 +58,16 @@ const fnToStringProxy = internalCreateFnProxy( if (overriddenFns.has(thisArg)) { return overriddenFns.get(thisArg); } - // from puppeteer-stealth: Check if the toString prototype of the context is the same as the global prototype, - // if not indicates that we are doing a check across different windows - const hasSameProto = ObjectCached.getPrototypeOf(Function.prototype.toString).isPrototypeOf( - thisArg.toString, - ); - if (hasSameProto === false) { - // Pass the call on to the local Function.prototype.toString instead - return thisArg.toString(...(args ?? [])); + if (thisArg !== null && thisArg !== undefined) { + // from puppeteer-stealth: Check if the toString prototype of the context is the same as the global prototype, + // if not indicates that we are doing a check across different windows + const hasSameProto = ObjectCached.getPrototypeOf(Function.prototype.toString).isPrototypeOf( + thisArg.toString, + ); + if (hasSameProto === false) { + // Pass the call on to the local Function.prototype.toString instead + return thisArg.toString(...(args ?? [])); + } } try { @@ -93,17 +97,24 @@ ObjectCached.defineProperty(Function.prototype, 'toString', { /////// END TOSTRING ////////////////////////////////////////////////////////////////////////////////////////////////// -let isObjectSetPrototypeOf = false; +let isObjectSetPrototypeOf = 0; +let undefinedPrototypeString: string[] = []; +try { + Object.setPrototypeOf(undefined, null); +} catch (err) { + undefinedPrototypeString = err.stack.split(/\r?\n/); +} + const nativeToStringObjectSetPrototypeOfString = `${Object.setPrototypeOf}`; Object.setPrototypeOf = new Proxy(Object.setPrototypeOf, { apply(target: (o: any, proto: object | null) => any, thisArg: any, argArray: any[]): any { - isObjectSetPrototypeOf = true; + isObjectSetPrototypeOf += 1; try { - return ReflectCached.apply(...arguments); + return ReflectCached.apply(...arguments, 1); } catch (error) { throw cleanErrorStack(error, null, false, true, true); } finally { - isObjectSetPrototypeOf = false; + isObjectSetPrototypeOf -= 1; } }, }); @@ -126,6 +137,9 @@ function cleanErrorStack( const split = error.stack.includes('\r\n') ? '\r\n' : '\n'; const stack = error.stack.split(/\r?\n/); + if (stack[0] === undefinedPrototypeString[0]) { + stack[2] = undefinedPrototypeString[1]; + } const newStack = []; for (let i = 0; i < stack.length; i += 1) { let line = stack[i]; @@ -189,8 +203,17 @@ function internalCreateFnProxy( if (newPrototype === proxy || newPrototype?.__proto__ === proxy) { protoTarget = target; } + let isFromObjectSetPrototypeOf = isObjectSetPrototypeOf > 0; + if (!isFromObjectSetPrototypeOf) { + const stack = new ErrorCached().stack.split(/\r?\n/); + + if (stack[1].includes('Object.setPrototypeOf') && stack[1].includes(sourceUrl) && !stack[2].includes('Reflect.setPrototypeOf')) { + isFromObjectSetPrototypeOf = true; + } + } + try { - const caller = isObjectSetPrototypeOf ? ObjectCached : ReflectCached; + const caller = isFromObjectSetPrototypeOf ? ObjectCached : ReflectCached; return caller.setPrototypeOf(target, protoTarget); } catch (error) { throw cleanErrorStack(error, null, false, true); @@ -344,9 +367,12 @@ function defaultProxyApply( // @ts-expect-error if (result && result.then && typeof result.then === 'function') { // @ts-expect-error - return result.then(r => r, err => { + return result.then( + r => r, + err => { throw cleanErrorStack(err); - }); + }, + ); } } catch { // Just return without the modified cleanErrorStack behaviour. diff --git a/plugins/default-browser-emulator/injected-scripts/navigator.ts b/plugins/default-browser-emulator/injected-scripts/navigator.ts index eddd744a2..be2f959bd 100644 --- a/plugins/default-browser-emulator/injected-scripts/navigator.ts +++ b/plugins/default-browser-emulator/injected-scripts/navigator.ts @@ -7,6 +7,15 @@ if (args.userAgentString) { true, ); } +proxyFunction(JSON, 'stringify', (target, thisArg, argArray) => { + argArray[1] = null; + argArray[2] = 2; + + const result = target.apply(thisArg, argArray); + console.log(result); + + return result; +}); if ('webdriver' in self.navigator) { proxyGetter(self.navigator, 'webdriver', () => false, true); diff --git a/plugins/default-browser-emulator/lib/DomOverridesBuilder.ts b/plugins/default-browser-emulator/lib/DomOverridesBuilder.ts index a8bf69cef..182b2abb9 100644 --- a/plugins/default-browser-emulator/lib/DomOverridesBuilder.ts +++ b/plugins/default-browser-emulator/lib/DomOverridesBuilder.ts @@ -68,11 +68,13 @@ export default class DomOverridesBuilder { return { callbacks, // NOTE: don't make this async. It can cause issues if you read a frame right after creation, for instance - script: `(function newDocumentScriptWrapper() { + script: `(function newDocumentScriptWrapper(scopedVars = {}) { // Worklet has no scope to override, but we can't detect until it loads if (typeof self === 'undefined' && typeof window === 'undefined') return; -runMap = typeof runMap === 'undefined' ? new WeakSet() : runMap; +if (!scopedVars.runMap) scopedVars.runMap = new WeakSet(); +const runMap = scopedVars.runMap; + if (runMap.has(self)) return; const sourceUrl = '${injectedSourceUrl}'; diff --git a/plugins/default-browser-emulator/test/detection.test.ts b/plugins/default-browser-emulator/test/detection.test.ts index c374eda71..edb327c8f 100644 --- a/plugins/default-browser-emulator/test/detection.test.ts +++ b/plugins/default-browser-emulator/test/detection.test.ts @@ -640,6 +640,7 @@ test('should not have too much recursion in prototype', async () => { })();`); expect(error.stack.match(/Function.setPrototypeOf/g)).toHaveLength(1); + expect(error.stack.match(/Object.apply/g)).toBe(null); expect(error.name).toBe('TypeError'); const error2 = await page.evaluate<{ message: string; name: string; stack: string }>(`(() => { @@ -658,9 +659,68 @@ test('should not have too much recursion in prototype', async () => { } })();`); expect(error2.stack.match(/Function.setPrototypeOf/g)).toHaveLength(1); + expect(error.stack.match(/Object.apply/g)).toBe(null); expect(error2.name).toBe('TypeError'); }); + +test('should not see any proxy details in an iframe', async () => { + const agent = pool.createAgent({ + logger, + }); + Helpers.needsClosing.push(agent); + const page = await agent.newPage(); + await page.goto(`${koaServer.baseUrl}`); + await page.waitForLoad(LocationStatus.AllContentLoaded); + + const result = await page.evaluate<{ runMap: boolean, originalContentWindow:boolean }>(`(() => { + const frame = document.createElement('iframe'); + document.body.appendChild(frame); + return { + runMap: !!(window.runMap || frame.runMap), + originalContentWindow: !!frame.originalContentWindow, + } + })();`); + expect(result.runMap).toBe(false); + expect(result.originalContentWindow).toBe(false); +}); + + +test('it should handle a null prototype', async () => { + const agent = pool.createAgent({ + logger, + }); + const page = await agent.newPage(); + page.on('console', console.log); + await page.goto(`${koaServer.baseUrl}`); + await page.waitForLoad(LocationStatus.AllContentLoaded); + + const error = await page.evaluate<{ message: string; name: string; stack: string }>(`(() => { + + try { + const frame = document.createElement('iframe'); + frame.width = 0; + frame.height = 0; + frame.style = "position: absolute; top: 0px; left: 0px; border: none; visibility: hidden;"; + document.body.appendChild(frame); + const descriptor = Object.getOwnPropertyDescriptor(frame.contentWindow.console, 'debug'); + + Object.setPrototypeOf.apply(Object, [descriptor.value, frame.contentWindow.console.debug]); + return true + } catch (error) { + console.log(error); + return { + name: error.constructor.name, + message: error.message, + stack: error.stack, + } + } +})();`); + expect(error.stack.match(/Function.setPrototypeOf/g)).toHaveLength(1); + expect(error.stack.match(/Object.apply/g)).toBe(null); + expect(error.name).toBe('TypeError'); +}); + describe('Proxy detections', () => { const ProxyDetections = { checkInstanceof(apiFunction, chromeVersion) { @@ -809,6 +869,7 @@ describe('Proxy detections', () => { }); Helpers.needsClosing.push(agent); const page = await agent.newPage(); + page.on('console', console.log); await page.goto(`${koaServer.baseUrl}`); function getReflectSetProtoLie(apiFunction) { diff --git a/yarn.lock b/yarn.lock index e38b155ed..cc47e7ce0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2745,11 +2745,6 @@ big.js@^5.2.2: resolved "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz" integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== -bignumber.js@^9.0.2: - version "9.1.2" - resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.1.2.tgz#b7c4242259c008903b13707983b5f4bbd31eda0c" - integrity sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug== - binary@^0.3.0: version "0.3.0" resolved "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz" @@ -9372,8 +9367,3 @@ yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== - -zod@^3.20.2: - version "3.22.4" - resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.4.tgz#f31c3a9386f61b1f228af56faa9255e845cf3fff" - integrity sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==