Skip to content
This repository has been archived by the owner on Sep 16, 2024. It is now read-only.

Commit

Permalink
fix(plugins): don’t scope emulator vars in window
Browse files Browse the repository at this point in the history
fix(plugins): toString of navigator exploding
  • Loading branch information
blakebyrnes committed Mar 20, 2024
1 parent 08a61d5 commit 4775864
Show file tree
Hide file tree
Showing 6 changed files with 124 additions and 35 deletions.
Original file line number Diff line number Diff line change
@@ -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);
Expand Down
56 changes: 41 additions & 15 deletions plugins/default-browser-emulator/injected-scripts/_proxyUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
}
},
});
Expand All @@ -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];
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -344,9 +367,12 @@ function defaultProxyApply<T, K extends keyof T>(
// @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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
6 changes: 4 additions & 2 deletions plugins/default-browser-emulator/lib/DomOverridesBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}';
Expand Down
61 changes: 61 additions & 0 deletions plugins/default-browser-emulator/test/detection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }>(`(() => {
Expand All @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down
10 changes: 0 additions & 10 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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==

0 comments on commit 4775864

Please sign in to comment.