From 9ec622e4eef8d276c6c8e8e16a1cc625da013239 Mon Sep 17 00:00:00 2001 From: Blake Byrnes Date: Fri, 26 Apr 2024 10:48:04 -0400 Subject: [PATCH] fix: stack traces in utils --- .../injected-scripts/_proxyUtils.ts | 35 +- .../test/detection.test.ts | 509 ++++++++++-------- yarn.lock | 10 - 3 files changed, 302 insertions(+), 252 deletions(-) diff --git a/plugins/default-browser-emulator/injected-scripts/_proxyUtils.ts b/plugins/default-browser-emulator/injected-scripts/_proxyUtils.ts index 933ee4397..bad0378ed 100644 --- a/plugins/default-browser-emulator/injected-scripts/_proxyUtils.ts +++ b/plugins/default-browser-emulator/injected-scripts/_proxyUtils.ts @@ -112,11 +112,20 @@ Object.setPrototypeOf = new Proxy(Object.setPrototypeOf, { apply(): any { isObjectSetPrototypeOf += 1; try { - return ReflectCached.apply(...arguments, 1); + return ReflectCached.apply(...arguments); } catch (error) { throw cleanErrorStack(error, { stripStartingReflect: true, - skipDuplicate: /at .*setPrototypeOf/, + replaceLineFn(line, i) { + if (i === 1 && line.includes(' Proxy.setPrototypeOf')) { + return line.replace(' Proxy.setPrototypeOf', ' Object.setPrototypeOf'); + } + if (i === 1 && line.includes(' Function.setPrototypeOf')) { + return undefined; + } + return line; + + }, }); } finally { isObjectSetPrototypeOf -= 1; @@ -137,7 +146,6 @@ function cleanErrorStack( replaceLineFn?: (line: string, index: number) => string; startAfterSourceUrl?: boolean; stripStartingReflect?: boolean; - skipDuplicate?: RegExp; skipFirst?: RegExp; } = { startAfterSourceUrl: false, @@ -146,25 +154,21 @@ function cleanErrorStack( ) { if (!error.stack) return error; - const { replaceLineFn, startAfterSourceUrl, stripStartingReflect, skipDuplicate } = opts; + const { + replaceLineFn, + startAfterSourceUrl, + stripStartingReflect, + } = opts; 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 = []; - let matchedSkipDuplicate = false; for (let i = 0; i < stack.length; i += 1) { let line = stack[i]; - if (skipDuplicate) { - if (skipDuplicate.test(line)) { - if (matchedSkipDuplicate) continue; - matchedSkipDuplicate = true; - } else { - matchedSkipDuplicate = false; - } - } - if (stripStartingReflect && line.includes(' Reflect.')) continue; + + if (i === 1 && stripStartingReflect && line.includes(' Reflect.')) continue; if (line.includes(sourceUrl)) { if (startAfterSourceUrl === true) { newStack.length = 1; @@ -172,6 +176,7 @@ function cleanErrorStack( continue; } if (replaceLineFn) line = replaceLineFn(line, i); + if (!line) continue; newStack.push(line); } error.stack = newStack.join(split); @@ -240,7 +245,7 @@ function internalCreateFnProxy( const caller = isFromObjectSetPrototypeOf ? ObjectCached : ReflectCached; return caller.setPrototypeOf(target, protoTarget); } catch (error) { - throw cleanErrorStack(error, { stripStartingReflect: true }); + throw cleanErrorStack(error); } }, get(target: any, p: string | symbol, receiver: any): any { diff --git a/plugins/default-browser-emulator/test/detection.test.ts b/plugins/default-browser-emulator/test/detection.test.ts index 1c1b334f8..8cfbd4a39 100644 --- a/plugins/default-browser-emulator/test/detection.test.ts +++ b/plugins/default-browser-emulator/test/detection.test.ts @@ -129,51 +129,6 @@ test('should not be denied for notifications but prompt for permissions', async expect(permissions.permissionState).toBe('prompt'); }); -test('should not leave markers on permissions.query.toString', async () => { - const agent = pool.createAgent({ - logger, - }); - Helpers.needsClosing.push(agent); - const page = await agent.newPage(); - await page.goto(`${koaServer.baseUrl}`); - const perms: any = await page.evaluate(`(() => { - const permissions = window.navigator.permissions; - return { - hasDirectQueryProperty: permissions.hasOwnProperty('query'), - queryToString: permissions.query.toString(), - queryToStringToString: permissions.query.toString.toString(), - queryToStringHasProxyHandler: permissions.query.toString.hasOwnProperty('[[Handler]]'), - queryToStringHasProxyTarget: permissions.query.toString.hasOwnProperty('[[Target]]'), - queryToStringHasProxyRevoked: permissions.query.toString.hasOwnProperty('[[IsRevoked]]'), - } - })();`); - expect(perms.hasDirectQueryProperty).toBe(false); - expect(perms.queryToString).toBe('function query() { [native code] }'); - expect(perms.queryToStringToString).toBe('function toString() { [native code] }'); - expect(perms.queryToStringHasProxyHandler).toBe(false); - expect(perms.queryToStringHasProxyTarget).toBe(false); - expect(perms.queryToStringHasProxyRevoked).toBe(false); -}); - -test('should not recurse the toString function', async () => { - const agent = pool.createAgent({ - logger, - }); - Helpers.needsClosing.push(agent); - const page = await agent.newPage(); - await page.goto(`${koaServer.baseUrl}`); - const isHeadless = await page.evaluate(`(() => { - let gotYou = 0; - const spooky = /./; - spooky.toString = function() { - gotYou += 1; - return 'spooky'; - }; - console.debug(spooky); - return gotYou > 1; - })();`); - expect(isHeadless).toBe(false); -}); test('should not call evaluate on a stack getter in debug', async () => { const agent = pool.createAgent({ @@ -292,33 +247,6 @@ test('should not see polyfill error overrides', async () => { expect(result).not.toContain('anonymuos'); }); -test('cannot detect a proxy of args passed into a proxied function', async () => { - const agent = pool.createAgent({ - logger, - }); - Helpers.needsClosing.push(agent); - const page = await agent.newPage(); - page.on('console', console.log); - await page.goto(`${koaServer.baseUrl}`); - const result = await page.evaluate<{ path: string; result: string }>(`(async () => { - let path = '' - const proxyOfArgs = new Proxy([37445], { - get(target,prop, receiver) { - path = new Error().stack.slice(8); - return Reflect.get(target,prop, receiver) - } - }) - - const canvas = document.createElement("canvas"); - const gl = canvas.getContext("webgl"); - gl.getExtension('WEBGL_debug_renderer_info') - const result = gl.getParameter.apply(gl, proxyOfArgs); - return { path, result }; - })()`); - expect(result.path).not.toContain(''); - expect(result.result).toBe('Intel Inc.'); -}); - test('should get the correct platform from a nested cross-domain srcdoc iframe', async () => { koaServer.get('/nested-platform', ctx => { ctx.body = `

hi

@@ -549,6 +477,52 @@ test('should properly maintain stack traces in toString', async () => { expect(proxiedGetterStack.stack.split('\n')[1]).toContain('at Object.toString'); }, 120e3); +test('should not leave stack trace markers on permissions.query.toString', async () => { + const agent = pool.createAgent({ + logger, + }); + Helpers.needsClosing.push(agent); + const page = await agent.newPage(); + await page.goto(`${koaServer.baseUrl}`); + const perms: any = await page.evaluate(`(() => { + const permissions = window.navigator.permissions; + return { + hasDirectQueryProperty: permissions.hasOwnProperty('query'), + queryToString: permissions.query.toString(), + queryToStringToString: permissions.query.toString.toString(), + queryToStringHasProxyHandler: permissions.query.toString.hasOwnProperty('[[Handler]]'), + queryToStringHasProxyTarget: permissions.query.toString.hasOwnProperty('[[Target]]'), + queryToStringHasProxyRevoked: permissions.query.toString.hasOwnProperty('[[IsRevoked]]'), + } + })();`); + expect(perms.hasDirectQueryProperty).toBe(false); + expect(perms.queryToString).toBe('function query() { [native code] }'); + expect(perms.queryToStringToString).toBe('function toString() { [native code] }'); + expect(perms.queryToStringHasProxyHandler).toBe(false); + expect(perms.queryToStringHasProxyTarget).toBe(false); + expect(perms.queryToStringHasProxyRevoked).toBe(false); +}); + +test('should not recurse the toString function', async () => { + const agent = pool.createAgent({ + logger, + }); + Helpers.needsClosing.push(agent); + const page = await agent.newPage(); + await page.goto(`${koaServer.baseUrl}`); + const isHeadless = await page.evaluate(`(() => { + let gotYou = 0; + const spooky = /./; + spooky.toString = function() { + gotYou += 1; + return 'spooky'; + }; + console.debug(spooky); + return gotYou > 1; + })();`); + expect(isHeadless).toBe(false); +}); + // https://github.com/digitalhurricane-io/puppeteer-detection-100-percent test('should not leave stack trace markers when calling getJsValue', async () => { const agent = pool.createAgent({ @@ -614,76 +588,6 @@ test('should not leave stack trace markers when calling in page functions', asyn ); }); -test('should not have too much recursion in prototype', 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 error = await page.evaluate<{ message: string; name: string; stack: string }>(`(() => { - const apiFunction = Object.getOwnPropertyDescriptor(Navigator.prototype, 'deviceMemory').get; - - try { - Object.setPrototypeOf(apiFunction, apiFunction) + '' - 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'); - - const error2 = await page.evaluate<{ message: string; name: string; stack: string }>(`(() => { - const apiFunction = WebGL2RenderingContext.prototype.getParameter; - - try { - Object.setPrototypeOf(apiFunction, apiFunction) + '' - return true - } catch (error) { - console.log(error) - return { - name: error.constructor.name, - message: error.message, - stack: error.stack, - } - } -})();`); - 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('should get correct outerWidth for frame', async () => { const agent = pool.createAgent({ logger, @@ -714,91 +618,6 @@ test('should get correct outerWidth for frame', async () => { expect(result.outerWidth).toBeGreaterThanOrEqual(result.innerWidth); }); -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) { - 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'); -}); - -test('it should handle an undefined setPrototype', 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 { - Object.setPrototypeOf.call(undefined, () => {}) - } catch (error) { - return { - name: error.constructor.name, - message: error.message, - stack: error.stack, - } - } -})();`); - expect(error.stack.match(/at setPrototypeOf/g)).toHaveLength(1); - expect(error.name).toBe('TypeError'); -}); - -test('it should handle an undefined setPrototype for fn', 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 { - Object.setPrototypeOf(undefined, []) - } catch (error) { - console.log(error); - return { - name: error.constructor.name, - message: error.message, - stack: error.stack, - } - } -})();`); - expect(error.stack.match(/at Function.setPrototypeOf/g)).toHaveLength(1); - expect(error.name).toBe('TypeError'); -}); - describe('Proxy detections', () => { const ProxyDetections = { checkInstanceof(apiFunction, chromeVersion) { @@ -998,6 +817,242 @@ describe('Proxy detections', () => { expect(error.stack).not.toContain('Reflect'); expect(error.stack.split('\n').length).toBeGreaterThan(1); }); + + test('should handle an undefined setPrototype', 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 { + Object.setPrototypeOf.call(undefined, () => {}) + } catch (error) { + return { + name: error.constructor.name, + message: error.message, + stack: error.stack, + } + } +})();`); + expect(error.stack.match(/at setPrototypeOf/g)).toHaveLength(1); + expect(error.name).toBe('TypeError'); + }); + + test('should handle an undefined setPrototype for fn', 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 { + Object.setPrototypeOf(undefined, []) + } catch (error) { + return { + name: error.constructor.name, + message: error.message, + stack: error.stack, + } + } +})();`); + expect(error.stack.match(/at Function.setPrototypeOf/g)).toHaveLength(1); + expect(error.name).toBe('TypeError'); + }); + + test('should handle setPrototype.call with undefined', 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 { + Object.setPrototypeOf.call(console.debug, console.debug, undefined) + return true + } catch (error) { + return { + name: error.constructor.name, + message: error.message, + stack: error.stack, + } + } +})();`); + expect(error.stack.match(/at Proxy.setPrototypeOf/g)).toBeNull(); + expect(error.stack.match(/at Object.setPrototypeOf/g)).toHaveLength(1); + expect(error.name).toBe('TypeError'); + }); + + test('should correctly bubble anonymous object 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 { + Object.setPrototypeOf.apply({}, [console.debug, console.debug]) + return true + } catch (error) { + return { + name: error.constructor.name, + message: error.message, + stack: error.stack, + } + } +})();`); + expect(error.stack.match(/at Object.setPrototypeOf/g)).toHaveLength(1); + expect(error.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('cannot detect a proxy of args passed into a proxied function', async () => { + const agent = pool.createAgent({ + logger, + }); + Helpers.needsClosing.push(agent); + const page = await agent.newPage(); + page.on('console', console.log); + await page.goto(`${koaServer.baseUrl}`); + const result = await page.evaluate<{ path: string; result: string }>(`(async () => { + let path = '' + const proxyOfArgs = new Proxy([37445], { + get(target,prop, receiver) { + path = new Error().stack.slice(8); + return Reflect.get(target,prop, receiver) + } + }) + + const canvas = document.createElement("canvas"); + const gl = canvas.getContext("webgl"); + gl.getExtension('WEBGL_debug_renderer_info') + const result = gl.getParameter.apply(gl, proxyOfArgs); + return { path, result }; + })()`); + expect(result.path).not.toContain(''); + expect(result.result).toBe('Intel Inc.'); + }); + + test('should not have too much recursion in prototype', 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 error = await page.evaluate<{ message: string; name: string; stack: string }>(`(() => { + const apiFunction = Object.getOwnPropertyDescriptor(Navigator.prototype, 'deviceMemory').get; + + try { + Object.setPrototypeOf(apiFunction, apiFunction) + '' + return true + } catch (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'); + + const error2 = await page.evaluate<{ message: string; name: string; stack: string }>(`(() => { + const apiFunction = WebGL2RenderingContext.prototype.getParameter; + + try { + Object.setPrototypeOf(apiFunction, apiFunction) + '' + return true + } catch (error) { + console.log(error) + return { + name: error.constructor.name, + message: error.message, + stack: error.stack, + } + } +})();`); + 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 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) { + 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'); + }); }); it('should emulate in a shared worker', async () => { 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==