feat(coding-agent): enhanced browser stealth and automation capabilities

- Overhauled stealth spoofing mechanisms for WebGL, screen dimensions, Web workers, and iframe contexts using prototype-aware injection.
- Centralized function string representation patching to improve mimicry of native browser behavior across global objects.
- Patched puppeteer-core to remove detectable evaluation markers and implement lazy, pull-style execution context management.
- Enabled support for capturing LLM request JSON dumps and adjusted launcher flags to improve organic request patterns.
This commit is contained in:
can1357
2026-06-21 03:54:40 +02:00
parent 27f24f8511
commit 57cb7447e0
19 changed files with 1951 additions and 459 deletions
+1
View File
@@ -324,6 +324,7 @@
},
"patchedDependencies": {
"@ark/schema@0.56.0": "patches/@ark%2Fschema@0.56.0.patch",
"puppeteer-core@25.1.0": "patches/puppeteer-core@25.1.0.patch",
},
"catalog": {
"@agentclientprotocol/sdk": "0.25.0",
+2 -1
View File
@@ -5,7 +5,8 @@
"type": "module",
"packageManager": "bun@1.3.14",
"patchedDependencies": {
"@ark/schema@0.56.0": "patches/@ark%2Fschema@0.56.0.patch"
"@ark/schema@0.56.0": "patches/@ark%2Fschema@0.56.0.patch",
"puppeteer-core@25.1.0": "patches/puppeteer-core@25.1.0.patch"
},
"workspaces": {
"packages": [
+7
View File
@@ -1,10 +1,17 @@
# Changelog
## [Unreleased]
### Added
- Added LLM request JSON export functionality to `/dump`
### Changed
- Improved browser stealth by suppressing common automation flags and patching property descriptors
- Enhanced stealth for `WebGL`, `Worker`, `IFrame`, `Screen`, and `Audio` APIs to evade detection
- Updated `toString` patching to register native function sources for improved fingerprint protection
### Removed
- Removed `/debug dump-next-request` command
+107 -31
View File
@@ -29,10 +29,23 @@ export const DEFAULT_VIEWPORT = { width: 1365, height: 768, deviceScaleFactor: 1
* connection dropped, etc.).
*/
export const BROWSER_PROTOCOL_TIMEOUT_MS = 60_000;
// Automation-tell launch flags that puppeteer-core adds by default. We suppress
// them via `ignoreDefaultArgs` (the supported escape hatch) to mirror xxxx's
// chromiumSwitches patch. `--enable-automation` is the loudest: it sets
// navigator.webdriver=true and shows the "controlled by automated software" infobar.
// `ignoreDefaultArgs` does exact-string matching, so each entry must be a flag that
// puppeteer emits verbatim. The default `--disable-features=...` string can't be
// matched this way; it is neutralized in the puppeteer-core patch (ChromeLauncher).
const STEALTH_IGNORE_DEFAULT_ARGS = [
"--enable-automation",
"--disable-extensions",
"--disable-default-apps",
"--disable-component-extensions-with-background-pages",
"--disable-popup-blocking",
"--disable-client-side-phishing-detection",
"--allow-pre-commit-input",
"--disable-ipc-flooding-protection",
"--metrics-recording-only",
];
const STEALTH_ACCEPT_LANGUAGE = "en-US,en";
@@ -309,9 +322,11 @@ export interface UserAgentOverride {
userAgentMetadata: {
brands: Array<{ brand: string; version: string }>;
fullVersion: string;
fullVersionList: Array<{ brand: string; version: string }>;
platform: string;
platformVersion: string;
architecture: string;
bitness: string;
model: string;
mobile: boolean;
};
@@ -371,6 +386,26 @@ function patchSourceUrl(page: Page): void {
};
}
async function resolveMacOsProductVersion(): Promise<string> {
if (os.platform() !== "darwin") return "";
try {
const plist = await Bun.file("/System/Library/CoreServices/SystemVersion.plist").text();
return plist.match(/<key>ProductVersion<\/key>\s*<string>([^<]+)<\/string>/)?.[1] ?? "";
} catch {
return "";
}
}
function resolveHostArchitecture(): string {
if (os.arch() === "arm64") return "arm";
if (os.arch().includes("64")) return "x86";
return "";
}
function resolveHostBitness(): string {
return os.arch().includes("64") ? "64" : "";
}
async function resolveUserAgentOverride(page: Page): Promise<UserAgentOverride> {
const rawUserAgent = await page.browser().userAgent();
let userAgent = rawUserAgent.replace("HeadlessChrome/", "Chrome/");
@@ -379,32 +414,24 @@ async function resolveUserAgentOverride(page: Page): Promise<UserAgentOverride>
}
const uaVersionMatch = userAgent.match(/Chrome\/([\d|.]+)/);
const fallbackVersionMatch = uaVersionMatch ?? (await page.browser().version()).match(/\/([\d|.]+)/);
const uaVersion = fallbackVersionMatch?.[1] ?? "0";
const majorVersion = Number.parseInt(uaVersion.split(".")[0] ?? "0", 10) || 0;
const browserVersionMatch = (await page.browser().version()).match(/\/([\d|.]+)/);
const legacyVersion = uaVersionMatch?.[1] ?? browserVersionMatch?.[1] ?? "0";
const fullVersion = browserVersionMatch?.[1] ?? legacyVersion;
const majorVersion = Number.parseInt(legacyVersion.split(".")[0] ?? "0", 10) || 0;
const isAndroid = userAgent.includes("Android");
const platform = userAgent.includes("Mac OS X")
? "MacIntel"
: isAndroid
? "Android"
: userAgent.includes("Linux")
? "Linux"
: "Win32";
const platformFull = userAgent.includes("Mac OS X")
? "Mac OS X"
: isAndroid
? "Android"
: userAgent.includes("Linux")
? "Linux"
: "Windows";
const platformVersion = userAgent.includes("Mac OS X ")
? (userAgent.match(/Mac OS X ([^)]+)/)?.[1] ?? "")
const isMac = userAgent.includes("Mac OS X");
const isWindows = userAgent.includes("Windows");
const platform = isMac ? "MacIntel" : isAndroid ? "Android" : userAgent.includes("Linux") ? "Linux" : "Win32";
const platformFull = isMac ? "macOS" : isAndroid ? "Android" : userAgent.includes("Linux") ? "Linux" : "Windows";
const platformVersion = isMac
? await resolveMacOsProductVersion()
: userAgent.includes("Android ")
? (userAgent.match(/Android ([^;]+)/)?.[1] ?? "")
: userAgent.includes("Windows ")
? (userAgent.match(/Windows .*?([\d|.]+);?/)?.[1] ?? "")
: isWindows
? (userAgent.match(/Windows NT ([\d.]+)/)?.[1] ?? "")
: "";
const architecture = isAndroid ? "" : "x86";
const architecture = isAndroid ? "" : resolveHostArchitecture();
const bitness = isAndroid ? "" : resolveHostBitness();
const model = isAndroid ? (userAgent.match(/Android.*?;\s([^)]+)/)?.[1] ?? "") : "";
const brandOrders = [
@@ -422,6 +449,10 @@ async function resolveUserAgentOverride(page: Page): Promise<UserAgentOverride>
brands[order[0]!] = { brand: greaseyBrand, version: "99" };
brands[order[1]!] = { brand: "Chromium", version: String(majorVersion) };
brands[order[2]!] = { brand: "Google Chrome", version: String(majorVersion) };
const fullVersionList = brands.map(({ brand }) => ({
brand,
version: brand === greaseyBrand ? "99.0.0.0" : fullVersion,
}));
return {
userAgent,
@@ -429,10 +460,12 @@ async function resolveUserAgentOverride(page: Page): Promise<UserAgentOverride>
acceptLanguage: STEALTH_ACCEPT_LANGUAGE,
userAgentMetadata: {
brands,
fullVersion: uaVersion,
fullVersion,
fullVersionList,
platform: platformFull,
platformVersion,
architecture,
bitness,
model,
mobile: isAndroid,
},
@@ -578,15 +611,29 @@ function buildStealthInjectionScript(scripts: readonly string[] = STEALTH_PATCH_
.join(";\n");
return `(() => {
// Native function cache - captured before any tampering
const iframe = document.createElement("iframe");
iframe.style.display = "none";
const Page_Function_toString = Function.prototype.toString;
const Page_FunctionToStringDescriptor = Object.getOwnPropertyDescriptor(Function.prototype, "toString");
const Page_Proxy = Proxy;
const Page_WeakMap = WeakMap;
const Page_WeakMap_get = Page_WeakMap.prototype.get;
const Page_WeakMap_set = Page_WeakMap.prototype.set;
// Native function cache - captured before any tampering.
// A same-origin iframe yields natives uncontaminated by page-level
// tampering, but at document-start (when this preload runs) there is
// no documentElement to attach it to. In that case the page itself
// hasn't executed yet, so window's own natives are still pristine —
// fall back to window instead of bailing, otherwise none of the
// fingerprint patches below would ever run.
let iframe = null;
const container = document.head ?? document.documentElement;
if (!container) return;
container.appendChild(iframe);
if (container) {
iframe = document.createElement("iframe");
iframe.style.display = "none";
container.appendChild(iframe);
if (!iframe.contentWindow) iframe = null;
}
try {
const nativeWindow = iframe.contentWindow;
if (!nativeWindow) return;
const nativeWindow = iframe ? iframe.contentWindow : window;
// Cache pristine native functions
const Function_toString = nativeWindow.Function.prototype.toString;
@@ -626,9 +673,38 @@ function buildStealthInjectionScript(scripts: readonly string[] = STEALTH_PATCH_
const Intl_DateTimeFormat = nativeWindow.Intl.DateTimeFormat;
const Date_constructor = nativeWindow.Date;
const nativeFunctionSources = new Page_WeakMap();
const makeNativeString = (name) => "function " + (name || "") + "() { [native code] }";
const registerNativeSource = (fn, source) => {
if (typeof fn === "function") Reflect_apply(Page_WeakMap_set, nativeFunctionSources, [fn, source]);
return fn;
};
const patchToString = (fn, name) => registerNativeSource(fn, makeNativeString(name));
if (${scripts.length > 0 ? "true" : "false"}) {
const functionToStringProxy = new Page_Proxy(Page_Function_toString, {
apply(target, thisArg, args) {
const source = Reflect_apply(Page_WeakMap_get, nativeFunctionSources, [thisArg]);
if (source) return source;
return Reflect_apply(target, thisArg, args || []);
},
get(target, key, receiver) {
return Reflect_get(target, key, receiver);
},
});
registerNativeSource(functionToStringProxy, makeNativeString("toString"));
Object_defineProperty(Function.prototype, "toString", {
...(Page_FunctionToStringDescriptor || {
writable: true,
configurable: true,
enumerable: false,
}),
value: functionToStringProxy,
});
}
${joint}
} finally {
if (iframe.parentNode) iframe.parentNode.removeChild(iframe);
if (iframe && iframe.parentNode) iframe.parentNode.removeChild(iframe);
}})();`;
}
@@ -1,8 +1,5 @@
// Helper to generate native code string
const makeNativeString = (name) =>
"function " + (name || "") + "() { [native code] }";
// Patch toString for common fingerprinted functions
// Register native-looking source for common fingerprinted functions without
// adding own toString properties.
const patchedFns = [
[window.alert, "alert"],
[window.prompt, "prompt"],
@@ -20,44 +17,28 @@ const patchedFns = [
];
for (const [fn, name] of patchedFns) {
if (typeof fn === "function") {
const nativeStr = makeNativeString(name);
Object_defineProperty(fn, "toString", {
value: function toString() { return nativeStr; },
writable: false,
configurable: true,
enumerable: false,
});
}
patchToString(fn, name);
}
// Patch Object.getOwnPropertyDescriptor to return native-looking descriptors
Object.getOwnPropertyDescriptor = function (obj, prop) {
const descriptor = Object_getOwnPropertyDescriptor.call(this, obj, prop);
// Patch Object.getOwnPropertyDescriptor to return native-looking accessor
// source through the shared Function.prototype.toString registry.
const patchedGetOwnPropertyDescriptor = function getOwnPropertyDescriptor(obj, prop) {
const descriptor = Reflect_apply(Object_getOwnPropertyDescriptor, this, [obj, prop]);
if (!descriptor) return descriptor;
// Make patched descriptors look native
if (descriptor.get && typeof descriptor.get === "function") {
const getStr = makeNativeString("get " + String(prop));
Object_defineProperty(descriptor.get, "toString", {
value: function toString() { return getStr; },
writable: false,
configurable: true,
enumerable: false,
});
patchToString(descriptor.get, "get " + String(prop));
}
if (descriptor.set && typeof descriptor.set === "function") {
const setStr = makeNativeString("set " + String(prop));
Object_defineProperty(descriptor.set, "toString", {
value: function toString() { return setStr; },
writable: false,
configurable: true,
enumerable: false,
});
patchToString(descriptor.set, "set " + String(prop));
}
return descriptor;
};
// Cleanup
document.head.removeChild(iframe);
patchToString(patchedGetOwnPropertyDescriptor, "getOwnPropertyDescriptor");
Object_defineProperty(Object, "getOwnPropertyDescriptor", {
value: patchedGetOwnPropertyDescriptor,
writable: true,
configurable: true,
enumerable: false,
});
@@ -1,20 +1,80 @@
const scheduleActivity = () => {
const delay = 3000 + Math_random() * 4000;
Window_setTimeout(() => {
Object_defineProperty(document, "hidden", { get: () => false });
Object_defineProperty(document, "visibilityState", {
get: () => "visible",
{
const visibilityDescriptors = Object_getOwnPropertyDescriptors({
get hidden() {
return false;
},
get visibilityState() {
return "visible";
},
get webkitHidden() {
return false;
},
get webkitVisibilityState() {
return "visible";
},
});
for (const key of Object_keys(visibilityDescriptors)) {
const descriptor = visibilityDescriptors[key];
if (descriptor && typeof descriptor.get === "function") {
patchToString(descriptor.get, "get " + key);
}
}
const focusDescriptor = Object_getOwnPropertyDescriptor(
{
hasFocus() {
return document.visibilityState === "visible";
},
},
"hasFocus",
);
if (focusDescriptor && typeof focusDescriptor.value === "function") {
patchToString(focusDescriptor.value, "hasFocus");
}
const clearOwnSlot = (name) => {
const ownDescriptor = Object_getOwnPropertyDescriptor(document, name);
if (!ownDescriptor) return true;
if (ownDescriptor.configurable !== true) return false;
return Reflect_deleteProperty(document, name);
};
const inheritedSlot = (name, expectedKind) => {
let proto = Object_getPrototypeOf(document);
while (proto) {
const descriptor = Object_getOwnPropertyDescriptor(proto, name);
if (descriptor && typeof descriptor[expectedKind] === "function") {
return [proto, descriptor];
}
proto = Object_getPrototypeOf(proto);
}
};
const defineAccessor = (name) => {
if (!clearOwnSlot(name)) return;
const slot = inheritedSlot(name, "get");
if (!slot || slot[1].configurable !== true) return;
Object_defineProperty(slot[0], name, {
get: visibilityDescriptors[name].get,
enumerable: slot[1].enumerable,
configurable: true,
});
Object_defineProperty(document, "webkitVisibilityState", {
get: () => "visible",
});
document.dispatchEvent(new Window_Event("visibilitychange"));
if (Math_random() < 0.4) window.dispatchEvent(new Window_Event("focus"));
document.hasFocus = () => true;
if (Math_random() < 0.3) window.dispatchEvent(new Window_Event("scroll"));
if (navigator.wakeLock)
navigator.wakeLock.request("screen").catch(() => {});
scheduleActivity();
}, delay);
};
scheduleActivity();
};
defineAccessor("hidden");
defineAccessor("visibilityState");
defineAccessor("webkitHidden");
defineAccessor("webkitVisibilityState");
if (clearOwnSlot("hasFocus")) {
const slot = inheritedSlot("hasFocus", "value");
if (slot && slot[1].configurable === true) {
Object_defineProperty(slot[0], "hasFocus", {
value: focusDescriptor.value,
writable: slot[1].writable === true,
enumerable: slot[1].enumerable,
configurable: true,
});
}
}
}
@@ -1,11 +1,57 @@
const elementDescriptor = Object_getOwnPropertyDescriptor(
HTMLElement.prototype,
"offsetHeight",
);
Object_defineProperty(HTMLDivElement.prototype, "offsetHeight", {
...elementDescriptor,
get: function () {
if (this.id === "modernizr") return 1;
return Reflect_apply(elementDescriptor.get, this, []);
},
});
{
const htmlElementOffsetHeightDescriptor =
typeof HTMLElement === "undefined"
? undefined
: Object_getOwnPropertyDescriptor(HTMLElement.prototype, "offsetHeight");
const htmlDivElementOffsetHeightDescriptor =
typeof HTMLDivElement === "undefined"
? undefined
: Object_getOwnPropertyDescriptor(HTMLDivElement.prototype, "offsetHeight");
const offsetHeightDescriptor =
htmlDivElementOffsetHeightDescriptor || htmlElementOffsetHeightDescriptor;
const offsetHeightPrototype = htmlDivElementOffsetHeightDescriptor
? HTMLDivElement.prototype
: htmlElementOffsetHeightDescriptor
? HTMLElement.prototype
: undefined;
const elementIdDescriptor =
typeof Element === "undefined"
? undefined
: Object_getOwnPropertyDescriptor(Element.prototype, "id");
if (
typeof HTMLDivElement !== "undefined" &&
offsetHeightPrototype &&
offsetHeightDescriptor &&
typeof offsetHeightDescriptor.get === "function" &&
offsetHeightDescriptor.configurable &&
elementIdDescriptor &&
typeof elementIdDescriptor.get === "function"
) {
const offsetHeightGetter = new Window_Proxy(offsetHeightDescriptor.get, {
apply(target, thisArg, args) {
const height = Reflect_apply(target, thisArg, args);
if (
height === 0 &&
Object_getPrototypeOf(thisArg) === HTMLDivElement.prototype &&
Reflect_apply(elementIdDescriptor.get, thisArg, []) === "modernizr"
) {
return 1;
}
return height;
},
});
patchToString(offsetHeightGetter, "get offsetHeight");
Object_defineProperty(
offsetHeightPrototype,
"offsetHeight",
Object_assign({}, offsetHeightDescriptor, {
get: offsetHeightGetter,
}),
);
}
}
@@ -1,15 +1,3 @@
const makeNativeString = (name) => "function " + (name || "") + "() { [native code] }";
const patchToString = (fn, name) => {
if (typeof fn !== "function") return;
Object_defineProperty(fn, "toString", {
value: function toString() {
return makeNativeString(name);
},
writable: false,
configurable: true,
enumerable: false,
});
};
// Ensure navigator.webdriver behaves like real Chrome
if (navigator.webdriver !== false && navigator.webdriver !== undefined) {
@@ -356,13 +344,19 @@ if (window.chrome && !("runtime" in window.chrome) && isSecureOrigin) {
// Suppress Permission.query for automation-controlled
if (navigator.permissions?.query) {
if (isSecureOrigin && "Notification" in window) {
const notificationPermissionGetter = Object_getOwnPropertyDescriptor({
get permission() {
return "default";
},
}, "permission").get;
patchToString(notificationPermissionGetter, "get permission");
Object_defineProperty(Notification, "permission", {
get: () => "default",
get: notificationPermissionGetter,
configurable: true,
});
} else if (!isSecureOrigin) {
const originalQuery = navigator.permissions.query;
navigator.permissions.query = function (parameters) {
const patchedPermissionsQuery = function query(parameters) {
if (parameters?.name === "notifications") {
const status = { state: "denied", onchange: null };
if (typeof PermissionStatus !== "undefined") {
@@ -372,6 +366,8 @@ if (navigator.permissions?.query) {
}
return originalQuery.call(this, parameters);
};
patchToString(patchedPermissionsQuery, "query");
navigator.permissions.query = patchedPermissionsQuery;
}
}
@@ -1,81 +1,174 @@
const addContentWindowProxy = (iframe) => {
const contentWindowProxy = {
get(target, key) {
if (key === "self") return this;
if (key === "frameElement") return iframe;
if (key === "0") return undefined;
return Reflect_get(target, key);
},
};
const iframeWindowProxies = new WeakMap();
if (!iframe.contentWindow) {
const proxy = new Window_Proxy(window, contentWindowProxy);
Object_defineProperty(iframe, "contentWindow", {
get() {
return proxy;
},
set(newValue) {
return newValue;
},
enumerable: true,
configurable: false,
});
const isFrameIndexKey = (key) => {
if (typeof key !== "string" || key === "") return false;
const number = +key;
return number >= 0 && number < 4294967295 && Math_floor(number) === number && `${number}` === key;
};
const getPropertyDescriptor = (object, key) => {
let current = object;
while (current) {
const descriptor = Object_getOwnPropertyDescriptor(current, key);
if (descriptor) return descriptor;
current = Object_getPrototypeOf(current);
}
};
const handleIframeCreation = (target, thisArg, args) => {
const iframe = Reflect_apply(target, thisArg, args);
const originalIframe = iframe;
const originalSrcdoc = originalIframe.srcdoc;
const descriptorWithValue = (target, key, value, writable) => {
const descriptor = Reflect_getOwnPropertyDescriptor(target, key);
if (descriptor && descriptor.configurable === false) return descriptor;
const next = descriptor ? Object_assign({}, descriptor) : { configurable: true, enumerable: true };
Reflect_deleteProperty(next, "get");
Reflect_deleteProperty(next, "set");
next.value = value;
if (!Reflect_has(next, "writable")) next.writable = writable;
return next;
};
const iframeContentWindowDescriptor =
typeof HTMLIFrameElement === "undefined"
? undefined
: getPropertyDescriptor(HTMLIFrameElement.prototype, "contentWindow");
const iframeSrcdocDescriptor =
typeof HTMLIFrameElement === "undefined"
? undefined
: getPropertyDescriptor(HTMLIFrameElement.prototype, "srcdoc");
Object_defineProperty(iframe, "srcdoc", {
configurable: true,
get() {
return originalSrcdoc;
},
set(newValue) {
addContentWindowProxy(this);
Object_defineProperty(iframe, "srcdoc", {
configurable: false,
writable: false,
value: originalSrcdoc,
});
originalIframe.srcdoc = newValue;
},
});
return iframe;
const getNativeContentWindow = (iframe) => {
if (iframeContentWindowDescriptor && iframeContentWindowDescriptor.get) {
return Reflect_apply(iframeContentWindowDescriptor.get, iframe, []);
}
return undefined;
};
const addIframeCreationSniffer = () => {
const originalCreateElement = document.createElement;
const handler = {
apply(target, thisArg, args) {
const isIframe = args && args.length && `${args[0]}`.toLowerCase() === "iframe";
if (!isIframe) {
return Reflect_apply(target, thisArg, args);
}
return handleIframeCreation(target, thisArg, args);
},
const addContentWindowProxy = (iframe) => {
let state = Reflect_apply(Page_WeakMap_get, iframeWindowProxies, [iframe]);
if (state) return state.proxy;
state = { proxy: undefined, target: undefined, wasConnected: false, setProxyTarget: undefined };
const isDiscarded = () => {
if (iframe.isConnected) state.wasConnected = true;
return state.wasConnected && !iframe.isConnected;
};
const currentFrameElement = () => {
if (iframe.isConnected) {
state.wasConnected = true;
return iframe;
}
return state.wasConnected ? null : iframe;
};
const contentWindowProxy = {
get(target, key) {
if (key === "self" || key === "window" || key === "frames" || key === "globalThis") return state.proxy;
if (key === "frameElement") return currentFrameElement();
if (key === "closed" && isDiscarded()) return true;
if (isFrameIndexKey(key)) return undefined;
if (key === "length") return 0;
return Reflect_get(target, key);
},
getOwnPropertyDescriptor(target, key) {
if (key === "self" || key === "window" || key === "frames" || key === "globalThis") {
return descriptorWithValue(target, key, state.proxy, true);
}
if (key === "frameElement") {
return descriptorWithValue(target, key, currentFrameElement(), false);
}
if (key === "closed" && isDiscarded()) {
return descriptorWithValue(target, key, true, false);
}
if (isFrameIndexKey(key)) return undefined;
if (key === "length") return descriptorWithValue(target, key, 0, false);
return Reflect_getOwnPropertyDescriptor(target, key);
},
has(target, key) {
if (key === "self" || key === "window" || key === "frames" || key === "globalThis" || key === "frameElement") return true;
if (isFrameIndexKey(key)) return false;
return Reflect_has(target, key);
},
ownKeys(target) {
const keys = Reflect_ownKeys(target);
const filtered = [];
for (let index = 0; index < keys.length; index++) {
if (!isFrameIndexKey(keys[index])) filtered.push(keys[index]);
}
return filtered;
},
};
const proxied = new Window_Proxy(originalCreateElement, handler);
Object_defineProperty(document, "createElement", {
value: proxied,
writable: true,
configurable: true,
});
Object_defineProperty(document.createElement, "toString", {
value: Function_toString.bind(originalCreateElement),
writable: false,
configurable: true,
enumerable: false,
});
};
const setProxyTarget = (target) => {
if (state.target === target && state.proxy) return state.proxy;
state.target = target;
state.proxy = new Window_Proxy(target, contentWindowProxy);
return state.proxy;
};
state.setProxyTarget = setProxyTarget;
const initialContentWindow = getNativeContentWindow(iframe);
if (initialContentWindow) setProxyTarget(initialContentWindow);
if (iframe.isConnected) state.wasConnected = true;
Reflect_apply(Page_WeakMap_set, iframeWindowProxies, [iframe, state]);
try {
addIframeCreationSniffer();
} catch {}
return state.proxy;
};
if (
iframeContentWindowDescriptor &&
iframeContentWindowDescriptor.get &&
iframeContentWindowDescriptor.configurable !== false
) {
const contentWindowAccessors = {
get contentWindow() {
const state = Reflect_apply(Page_WeakMap_get, iframeWindowProxies, [this]);
if (!state) return getNativeContentWindow(this);
if (this.isConnected) state.wasConnected = true;
if (state.wasConnected && !this.isConnected) return null;
const nativeContentWindow = getNativeContentWindow(this);
if (!nativeContentWindow) return nativeContentWindow;
return state.setProxyTarget(nativeContentWindow);
},
};
const contentWindowGetter = Object_getOwnPropertyDescriptor(contentWindowAccessors, "contentWindow").get;
patchToString(contentWindowGetter, "get contentWindow");
Object_defineProperty(HTMLIFrameElement.prototype, "contentWindow", {
get: contentWindowGetter,
set: iframeContentWindowDescriptor.set,
enumerable: iframeContentWindowDescriptor.enumerable,
configurable: iframeContentWindowDescriptor.configurable,
});
}
if (
iframeSrcdocDescriptor &&
iframeSrcdocDescriptor.configurable !== false
) {
const srcdocAccessors = {
get srcdoc() {
if (iframeSrcdocDescriptor.get) {
return Reflect_apply(iframeSrcdocDescriptor.get, this, []);
}
const value = this.getAttribute("srcdoc");
return value === null ? "" : value;
},
set srcdoc(newValue) {
addContentWindowProxy(this);
if (iframeSrcdocDescriptor.set) {
return Reflect_apply(iframeSrcdocDescriptor.set, this, [newValue]);
}
return this.setAttribute("srcdoc", newValue);
},
};
const srcdocDescriptor = Object_getOwnPropertyDescriptor(srcdocAccessors, "srcdoc");
const srcdocGetter = srcdocDescriptor.get;
const srcdocSetter = srcdocDescriptor.set;
patchToString(srcdocGetter, "get srcdoc");
patchToString(srcdocSetter, "set srcdoc");
Object_defineProperty(HTMLIFrameElement.prototype, "srcdoc", {
get: srcdocGetter,
set: srcdocSetter,
enumerable: iframeSrcdocDescriptor.enumerable,
configurable: iframeSrcdocDescriptor.configurable,
});
}
@@ -1,75 +1,233 @@
const vendors = ["Intel Inc.", "NVIDIA Corporation", "AMD"];
const renderers = [
"Intel(R) Iris(TM) Plus Graphics 640",
"Intel(R) HD Graphics 630",
"NVIDIA GeForce GTX 1050 Ti",
"NVIDIA GeForce GTX 1060",
"NVIDIA GeForce RTX 3060",
"AMD Radeon RX 580",
"AMD Radeon Pro 560",
];
const vendor = vendors[Math_floor(Math_random() * vendors.length)];
const renderer = renderers[Math_floor(Math_random() * renderers.length)];
const webglPlatformSource =
String(navigator.userAgentData?.platform || navigator.platform || "") +
" " +
String(navigator.userAgent || "");
const webglPlatformText = webglPlatformSource.toLowerCase();
const webglPlatformKind = webglPlatformText.includes("android")
? "android"
: webglPlatformText.includes("iphone") ||
webglPlatformText.includes("ipad") ||
webglPlatformText.includes("ipod")
? "ios"
: webglPlatformText.includes("mac")
? "mac"
: webglPlatformText.includes("win")
? "windows"
: webglPlatformText.includes("cros")
? "cros"
: "linux";
const getParameterProxyHandler = {
apply(target, thisArg, args) {
const param = args[0];
// VENDOR = 0x1F00
if (param === 0x1F00) return vendor;
// RENDERER = 0x1F01
if (param === 0x1F01) return renderer;
// UNMASKED_VENDOR_WEBGL = 0x9245
if (param === 0x9245) return vendor;
// UNMASKED_RENDERER_WEBGL = 0x9246
if (param === 0x9246) return renderer;
return Reflect_apply(target, thisArg, args);
},
const webglFallbackProfiles = {
android: {
vendor: "Qualcomm",
renderer: "Adreno (TM) 640",
},
ios: {
vendor: "Apple Inc.",
renderer: "Apple GPU",
},
mac: {
vendor: "Google Inc. (Intel Inc.)",
renderer: "ANGLE (Intel Inc., Intel(R) Iris(TM) Plus Graphics 640 OpenGL Engine, OpenGL 4.1)",
},
windows: {
vendor: "Google Inc. (Intel)",
renderer: "ANGLE (Intel, Intel(R) UHD Graphics 620 Direct3D11 vs_5_0 ps_5_0, D3D11)",
},
cros: {
vendor: "Google Inc. (Intel)",
renderer: "ANGLE (Intel, Mesa Intel(R) UHD Graphics 620 (KBL GT2), OpenGL 4.6)",
},
linux: {
vendor: "Google Inc. (Intel)",
renderer: "ANGLE (Intel, Mesa Intel(R) UHD Graphics 620 (KBL GT2), OpenGL 4.6)",
},
};
const webglFallbackProfile = webglFallbackProfiles[webglPlatformKind] || webglFallbackProfiles.linux;
const webglContextProfiles = new WeakMap();
const webglIsObjectKey = (value) =>
(typeof value === "object" && value !== null) || typeof value === "function";
const webglLooksSoftware = (value) => {
const text = String(value || "").toLowerCase();
return (
text.includes("swiftshader") ||
text.includes("llvmpipe") ||
text.includes("lavapipe") ||
text.includes("software") ||
text.includes("mesa offscreen") ||
text.includes("google inc. (google)")
);
};
// Hook WebGL contexts
const hookWebGL = (proto) => {
const originalGetParameter = proto.getParameter;
Object_defineProperty(proto, "getParameter", {
value: new Window_Proxy(originalGetParameter, getParameterProxyHandler),
writable: true,
configurable: true,
enumerable: true,
});
const webglMatchesPlatform = (renderer) => {
const text = String(renderer || "").toLowerCase();
if (webglPlatformKind === "windows") {
return !text.includes("apple") && !text.includes("mesa") && !text.includes("opengl engine");
}
if (webglPlatformKind === "mac") {
return !text.includes("direct3d") && !text.includes("d3d") && !text.includes("mesa");
}
if (webglPlatformKind === "android") {
return (
text.includes("adreno") ||
text.includes("mali") ||
text.includes("powervr") ||
text.includes("qualcomm")
);
}
if (webglPlatformKind === "ios") {
return text.includes("apple");
}
return !text.includes("direct3d") && !text.includes("d3d") && !text.includes("apple");
};
const webglGetContextProfile = (target, thisArg) => {
if (!webglIsObjectKey(thisArg)) return webglFallbackProfile;
const cached = Reflect_apply(Page_WeakMap_get, webglContextProfiles, [thisArg]);
if (cached) return cached;
let nativeVendor;
let nativeRenderer;
try {
nativeVendor = Reflect_apply(target, thisArg, [0x9245]);
nativeRenderer = Reflect_apply(target, thisArg, [0x9246]);
} catch {}
const nativeProfile =
typeof nativeVendor === "string" &&
typeof nativeRenderer === "string" &&
nativeVendor &&
nativeRenderer &&
!webglLooksSoftware(nativeVendor) &&
!webglLooksSoftware(nativeRenderer) &&
webglMatchesPlatform(nativeRenderer)
? { vendor: nativeVendor, renderer: nativeRenderer }
: webglFallbackProfile;
Reflect_apply(Page_WeakMap_set, webglContextProfiles, [thisArg, nativeProfile]);
return nativeProfile;
};
const webglGetParameterHandler = {
apply(target, thisArg, args) {
const nativeValue = Reflect_apply(target, thisArg, args);
const param = args[0];
if (param === 0x1f00 && typeof nativeValue === "string") return "WebKit";
if (param === 0x1f01 && typeof nativeValue === "string") return "WebKit WebGL";
if ((param === 0x9245 || param === 0x9246) && typeof nativeValue === "string") {
const profile = webglGetContextProfile(target, thisArg);
return param === 0x9245 ? profile.vendor : profile.renderer;
}
return nativeValue;
},
};
const webglFloatPrecisionTypes = {
0x8df0: true,
0x8df1: true,
0x8df2: true,
};
const webglClonePrecisionFormat = (result, values) => {
const ownKeys = Reflect_ownKeys(result);
let hasRangeMin = false;
let hasRangeMax = false;
let hasPrecision = false;
for (let index = 0; index < ownKeys.length; index += 1) {
if (ownKeys[index] === "rangeMin") hasRangeMin = true;
if (ownKeys[index] === "rangeMax") hasRangeMax = true;
if (ownKeys[index] === "precision") hasPrecision = true;
}
if (!hasRangeMin || !hasRangeMax || !hasPrecision) return result;
const clone = Object_create(Object_getPrototypeOf(result));
for (let index = 0; index < ownKeys.length; index += 1) {
const key = ownKeys[index];
const descriptor = Object_getOwnPropertyDescriptor(result, key);
if (!descriptor) return result;
if (key === "rangeMin" || key === "rangeMax" || key === "precision") {
if (!("value" in descriptor)) return result;
descriptor.value = values[key];
}
try {
Object_defineProperty(clone, key, descriptor);
} catch {
return result;
}
}
return clone;
};
const webglGetShaderPrecisionFormatHandler = {
apply(target, thisArg, args) {
const result = Reflect_apply(target, thisArg, args);
const precisionType = args[1];
if (
!result ||
webglPlatformKind === "android" ||
webglPlatformKind === "ios" ||
!webglFloatPrecisionTypes[precisionType]
) {
return result;
}
const rangeMin = result.rangeMin;
const rangeMax = result.rangeMax;
const precision = result.precision;
if (
typeof rangeMin !== "number" ||
typeof rangeMax !== "number" ||
typeof precision !== "number"
) {
return result;
}
const values = {
rangeMin: Math_max(rangeMin, 127),
rangeMax: Math_max(rangeMax, 127),
precision: Math_max(precision, 23),
};
if (
values.rangeMin === rangeMin &&
values.rangeMax === rangeMax &&
values.precision === precision
) {
return result;
}
return webglClonePrecisionFormat(result, values);
},
};
const webglInstallMethodProxy = (proto, name, handler) => {
if (!proto) return;
const descriptor = Object_getOwnPropertyDescriptor(proto, name);
if (!descriptor || typeof descriptor.value !== "function") return;
const proxy = new Window_Proxy(descriptor.value, handler);
patchToString(proxy, name);
try {
Object_defineProperty(proto, name, {
value: proxy,
writable: descriptor.writable,
configurable: descriptor.configurable,
enumerable: descriptor.enumerable,
});
} catch {}
};
if (window.WebGLRenderingContext) {
hookWebGL(WebGLRenderingContext.prototype);
webglInstallMethodProxy(WebGLRenderingContext.prototype, "getParameter", webglGetParameterHandler);
webglInstallMethodProxy(
WebGLRenderingContext.prototype,
"getShaderPrecisionFormat",
webglGetShaderPrecisionFormatHandler,
);
}
if (window.WebGL2RenderingContext) {
hookWebGL(WebGL2RenderingContext.prototype);
}
// Also mask getShaderPrecisionFormat for software rendering detection
const precisionMask = (proto) => {
const original = proto.getShaderPrecisionFormat;
if (!original) return;
Object_defineProperty(proto, "getShaderPrecisionFormat", {
value: function (shaderType, precisionType) {
const result = original.call(this, shaderType, precisionType);
if (result) {
// Hardware typically has higher precision than SwiftShader defaults
return {
precision: Math_max(result.precision, 23),
rangeMin: Math_min(result.rangeMin, 127),
rangeMax: Math_max(result.rangeMax, 127),
};
}
return result;
},
writable: true,
configurable: true,
enumerable: true,
});
};
if (window.WebGLRenderingContext) {
precisionMask(WebGLRenderingContext.prototype);
}
if (window.WebGL2RenderingContext) {
precisionMask(WebGL2RenderingContext.prototype);
webglInstallMethodProxy(WebGL2RenderingContext.prototype, "getParameter", webglGetParameterHandler);
webglInstallMethodProxy(
WebGL2RenderingContext.prototype,
"getShaderPrecisionFormat",
webglGetShaderPrecisionFormatHandler,
);
}
@@ -1,72 +1,260 @@
// Generate consistent "real" screen dimensions based on viewport
const width = window.innerWidth;
const height = window.innerHeight;
const availWidth = width;
const availHeight = Math_max(height - 40, 0); // Account for taskbar
const colorDepth = 24;
const pixelDepth = 24;
const devicePixelRatio = window.devicePixelRatio && window.devicePixelRatio > 1 ? window.devicePixelRatio : 1.25;
;(() => {
if (typeof Window_Proxy !== "function" || typeof Reflect_apply !== "function") return;
const defineScreenProp = (prop, value) => {
try {
Object_defineProperty(window.Screen?.prototype ?? window.screen, prop, {
get: () => value,
configurable: true,
enumerable: true,
});
} catch {}
};
const screenObject = window.screen;
if (!screenObject) return;
// Override screen properties
for (const [prop, descriptor] of Object_entries({
width,
height,
availWidth,
availHeight,
availLeft: 0,
availTop: 0,
colorDepth,
pixelDepth,
})) {
defineScreenProp(prop, descriptor);
}
const isFiniteNumber = (value) =>
typeof value === "number" &&
value === value &&
value !== Infinity &&
value !== -Infinity;
// Ensure outer dimensions match screen for consistency
const chromeFrameHeight = 85;
Object_defineProperty(window, "outerWidth", {
get: () => window.innerWidth,
configurable: true,
enumerable: true,
});
Object_defineProperty(window, "outerHeight", {
get: () => window.innerHeight + chromeFrameHeight,
configurable: true,
enumerable: true,
});
if (window.visualViewport) {
const defineVvpProp = (prop, value) => {
const readValue = (object, prop, fallback) => {
try {
Object_defineProperty(window.visualViewport, prop, {
get: () => value,
configurable: true,
enumerable: true,
});
} catch {}
const value = object[prop];
return value === undefined ? fallback : value;
} catch {
return fallback;
}
};
defineVvpProp("width", width);
defineVvpProp("height", height);
defineVvpProp("scale", 1);
defineVvpProp("offsetLeft", 0);
defineVvpProp("offsetTop", 0);
defineVvpProp("pageLeft", 0);
defineVvpProp("pageTop", 0);
}
const finiteNumber = (value, fallback) =>
isFiniteNumber(value) ? value : fallback;
// Consistent devicePixelRatio
Object_defineProperty(window, "devicePixelRatio", {
get: () => devicePixelRatio,
configurable: true,
enumerable: true,
});
const positiveNumber = (value, fallback) =>
isFiniteNumber(value) && value > 0 ? value : fallback;
const integerNumber = (value, fallback) =>
isFiniteNumber(value) ? Math_floor(value) : fallback;
const positiveInteger = (value, fallback) =>
isFiniteNumber(value) && value > 0 ? Math_floor(value) : fallback;
const findDescriptorOwner = (object, prop) => {
let owner = object;
while (owner) {
let descriptor;
try {
descriptor = Object_getOwnPropertyDescriptor(owner, prop);
} catch {
return undefined;
}
if (descriptor) return [owner, descriptor];
try {
owner = Object_getPrototypeOf(owner);
} catch {
return undefined;
}
}
return undefined;
};
const patchGetter = (object, prop, getValue) => {
const found = findDescriptorOwner(object, prop);
if (!found) return false;
const owner = found[0];
const descriptor = found[1];
if (descriptor.configurable !== true || typeof descriptor.get !== "function") {
return false;
}
const originalGet = descriptor.get;
const patchedGet = new Window_Proxy(originalGet, {
apply(target, thisArg, args) {
const nativeValue = Reflect_apply(target, thisArg, args);
return getValue(nativeValue, thisArg);
},
});
patchToString(patchedGet, "get " + prop);
try {
Object_defineProperty(owner, prop, {
configurable: descriptor.configurable,
enumerable: descriptor.enumerable,
get: patchedGet,
set: descriptor.set,
});
return true;
} catch {
return false;
}
};
const patchStableNumber = (object, prop, value) => {
const current = readValue(object, prop, undefined);
if (current === value) return;
patchGetter(object, prop, () => value);
};
const initialInnerWidth = positiveInteger(readValue(window, "innerWidth", 0), 0);
const initialInnerHeight = positiveInteger(readValue(window, "innerHeight", 0), 0);
const initialOuterWidth = positiveInteger(
readValue(window, "outerWidth", Math_max(initialInnerWidth, 1)),
Math_max(initialInnerWidth, 1),
);
const initialOuterHeight = positiveInteger(
readValue(window, "outerHeight", Math_max(initialInnerHeight, 1)),
Math_max(initialInnerHeight, 1),
);
// Real Chrome reserves vertical space for the tab strip + URL bar, so
// outerHeight is always taller than innerHeight (~88px on a stock window).
// Headless reports outerHeight === innerHeight, which is a well-known tell, so
// synthesize a realistic chrome height when the window has no visible chrome.
const browserChromeHeight = 88;
const targetOuterHeight =
initialOuterHeight > initialInnerHeight
? initialOuterHeight
: initialInnerHeight + browserChromeHeight;
const requiredWidth = Math_max(initialInnerWidth, initialOuterWidth, 1);
const requiredHeight = Math_max(initialInnerHeight, initialOuterHeight, targetOuterHeight, 1);
const screenWidth = Math_max(
positiveInteger(readValue(screenObject, "width", requiredWidth), requiredWidth),
requiredWidth,
);
const screenHeight = Math_max(
positiveInteger(readValue(screenObject, "height", requiredHeight), requiredHeight),
requiredHeight,
);
let screenAvailWidth = positiveInteger(
readValue(screenObject, "availWidth", screenWidth),
screenWidth,
);
if (screenAvailWidth < requiredWidth || screenAvailWidth > screenWidth) {
screenAvailWidth = screenWidth;
}
let screenAvailHeight = positiveInteger(
readValue(screenObject, "availHeight", screenHeight),
screenHeight,
);
if (screenAvailHeight < requiredHeight || screenAvailHeight > screenHeight) {
screenAvailHeight = screenHeight;
}
const screenAvailLeft = integerNumber(readValue(screenObject, "availLeft", 0), 0);
const screenAvailTop = integerNumber(readValue(screenObject, "availTop", 0), 0);
const screenColorDepth = positiveInteger(readValue(screenObject, "colorDepth", 24), 24);
const screenPixelDepth = positiveInteger(
readValue(screenObject, "pixelDepth", screenColorDepth),
screenColorDepth,
);
for (const entry of [
["width", screenWidth],
["height", screenHeight],
["availWidth", screenAvailWidth],
["availHeight", screenAvailHeight],
["availLeft", screenAvailLeft],
["availTop", screenAvailTop],
["colorDepth", screenColorDepth],
["pixelDepth", screenPixelDepth],
]) {
patchStableNumber(screenObject, entry[0], entry[1]);
}
const outerWidthNeedsPatch =
!isFiniteNumber(readValue(window, "outerWidth", undefined)) ||
readValue(window, "outerWidth", 0) <= 0 ||
Math_floor(readValue(window, "outerWidth", 0)) < initialInnerWidth;
if (outerWidthNeedsPatch) {
patchGetter(window, "outerWidth", (nativeValue) =>
Math_max(
positiveInteger(nativeValue, initialOuterWidth),
positiveInteger(readValue(window, "innerWidth", requiredWidth), requiredWidth),
),
);
}
const outerHeightNeedsPatch =
!isFiniteNumber(readValue(window, "outerHeight", undefined)) ||
readValue(window, "outerHeight", 0) <= 0 ||
Math_floor(readValue(window, "outerHeight", 0)) <= initialInnerHeight;
if (outerHeightNeedsPatch) {
patchGetter(window, "outerHeight", (nativeValue) =>
Math_max(
positiveInteger(nativeValue, initialOuterHeight),
positiveInteger(readValue(window, "innerHeight", requiredHeight), requiredHeight) +
browserChromeHeight,
),
);
}
const initialDevicePixelRatio = positiveNumber(
readValue(window, "devicePixelRatio", 1),
1,
);
if (readValue(window, "devicePixelRatio", undefined) !== initialDevicePixelRatio) {
patchGetter(window, "devicePixelRatio", () => initialDevicePixelRatio);
}
const visualViewportObject = readValue(window, "visualViewport", null);
if (visualViewportObject) {
const initialScale = positiveNumber(readValue(visualViewportObject, "scale", 1), 1);
if (readValue(visualViewportObject, "scale", undefined) !== initialScale) {
patchGetter(visualViewportObject, "scale", (nativeValue) =>
positiveNumber(nativeValue, initialScale),
);
}
const getViewportScale = () =>
positiveNumber(readValue(visualViewportObject, "scale", initialScale), initialScale);
const getViewportWidthFallback = () =>
positiveNumber(readValue(window, "innerWidth", requiredWidth), requiredWidth) /
getViewportScale();
const getViewportHeightFallback = () =>
positiveNumber(readValue(window, "innerHeight", requiredHeight), requiredHeight) /
getViewportScale();
if (positiveNumber(readValue(visualViewportObject, "width", 0), 0) <= 0) {
patchGetter(visualViewportObject, "width", (nativeValue) =>
positiveNumber(nativeValue, getViewportWidthFallback()),
);
}
if (positiveNumber(readValue(visualViewportObject, "height", 0), 0) <= 0) {
patchGetter(visualViewportObject, "height", (nativeValue) =>
positiveNumber(nativeValue, getViewportHeightFallback()),
);
}
const getViewportOffsetLeft = () =>
finiteNumber(readValue(visualViewportObject, "offsetLeft", 0), 0);
const getViewportOffsetTop = () =>
finiteNumber(readValue(visualViewportObject, "offsetTop", 0), 0);
if (!isFiniteNumber(readValue(visualViewportObject, "offsetLeft", undefined))) {
patchGetter(visualViewportObject, "offsetLeft", (nativeValue) =>
finiteNumber(nativeValue, 0),
);
}
if (!isFiniteNumber(readValue(visualViewportObject, "offsetTop", undefined))) {
patchGetter(visualViewportObject, "offsetTop", (nativeValue) =>
finiteNumber(nativeValue, 0),
);
}
if (!isFiniteNumber(readValue(visualViewportObject, "pageLeft", undefined))) {
patchGetter(visualViewportObject, "pageLeft", (nativeValue) =>
finiteNumber(
nativeValue,
finiteNumber(readValue(window, "scrollX", 0), 0) + getViewportOffsetLeft(),
),
);
}
if (!isFiniteNumber(readValue(visualViewportObject, "pageTop", undefined))) {
patchGetter(visualViewportObject, "pageTop", (nativeValue) =>
finiteNumber(
nativeValue,
finiteNumber(readValue(window, "scrollY", 0), 0) + getViewportOffsetTop(),
),
);
}
}
})();
@@ -47,7 +47,6 @@ const commonFonts = [
"Wingdings 3",
"Apple Color Emoji",
"Apple SD Gothic Neo",
"Helvetica Neue",
"Hoefler Text",
"Menlo",
"Monaco",
@@ -56,32 +55,31 @@ const commonFonts = [
"SF Pro Text",
];
// Override queryLocalFonts if present (Local Font Access API)
if ("queryLocalFonts" in window) {
const queryLocalFonts = async function queryLocalFonts() {
return commonFonts.map((family) => ({
family,
fullName: family,
postscriptName: family.replace(/\s+/g, ""),
style: "Regular",
blob: () => Promise_resolve(new Window_Blob([])),
}));
};
patchToString(queryLocalFonts, "queryLocalFonts");
Object_defineProperty(window, "queryLocalFonts", {
value: async () => {
return commonFonts.map((family) => ({
family,
fullName: family,
postscriptName: family.replace(/\s+/g, ""),
style: "Regular",
blob: () => Promise_resolve(new Window_Blob([])),
}));
},
value: queryLocalFonts,
writable: true,
configurable: true,
enumerable: true,
});
}
// Hide fonts-unique tracking via canvas
const originalGetContext = HTMLCanvasElement.prototype.getContext;
HTMLCanvasElement.prototype.getContext = function (type, options) {
const patchedGetContext = function getContext(type, options) {
const ctx = originalGetContext.call(this, type, options);
if (ctx && type === "2d") {
const originalFillText = ctx.fillText;
ctx.fillText = function (text, x, y, maxWidth) {
// Add tiny imperceptible noise to text rendering
const patchedFillText = function fillText(text, x, y, maxWidth) {
const noiseX = (Math_random() - 0.5) * 0.02;
const noiseY = (Math_random() - 0.5) * 0.02;
return originalFillText.call(
@@ -92,6 +90,10 @@ HTMLCanvasElement.prototype.getContext = function (type, options) {
maxWidth,
);
};
patchToString(patchedFillText, "fillText");
ctx.fillText = patchedFillText;
}
return ctx;
};
patchToString(patchedGetContext, "getContext");
HTMLCanvasElement.prototype.getContext = patchedGetContext;
@@ -1,32 +1,45 @@
// Spoof AudioContext latency values to look like real hardware
const spoofLatency = (proto) => {
Object_defineProperty(proto, "baseLatency", {
get: () => 0.005, // ~5ms typical for real hardware
configurable: true,
enumerable: true,
});
Object_defineProperty(proto, "outputLatency", {
get: () => 0.01, // ~10ms typical
// Spoof AudioContext latency values to look like real hardware.
const audioLatencyAccessors = {
get baseLatency() {
return 0.005;
},
get outputLatency() {
return 0.01;
},
get sampleRate() {
return 48000;
},
};
const defineAudioGetter = (proto, name) => {
const descriptor = Object_getOwnPropertyDescriptor(audioLatencyAccessors, name);
if (!descriptor || !descriptor.get) return;
patchToString(descriptor.get, "get " + name);
Object_defineProperty(proto, name, {
get: descriptor.get,
configurable: true,
enumerable: true,
});
};
const spoofLatency = (proto) => {
defineAudioGetter(proto, "baseLatency");
defineAudioGetter(proto, "outputLatency");
};
if (window.AudioContext) {
spoofLatency(AudioContext.prototype);
}
if (window.OfflineAudioContext) {
// For offline context, add subtle randomness to prevent deterministic fingerprints
const OriginalOfflineAudioContext = window.OfflineAudioContext;
window.OfflineAudioContext = class extends OriginalOfflineAudioContext {
const PatchedOfflineAudioContext = class OfflineAudioContext extends OriginalOfflineAudioContext {
constructor(numberOfChannels, length, sampleRate) {
super(numberOfChannels, length, sampleRate);
// Hook startRendering to add noise
const originalStartRendering = this.startRendering.bind(this);
this.startRendering = async () => {
const patchedStartRendering = async function startRendering() {
const buffer = await originalStartRendering();
// Add imperceptible noise to prevent deterministic hash
for (let c = 0; c < buffer.numberOfChannels; c++) {
const channel = buffer.getChannelData(c);
for (let i = 0; i < channel.length; i++) {
@@ -37,15 +50,14 @@ if (window.OfflineAudioContext) {
}
return buffer;
};
patchToString(patchedStartRendering, "startRendering");
this.startRendering = patchedStartRendering;
}
};
patchToString(PatchedOfflineAudioContext, "OfflineAudioContext");
window.OfflineAudioContext = PatchedOfflineAudioContext;
}
// Also spoof sampleRate consistency
if (window.AudioContext) {
Object_defineProperty(AudioContext.prototype, "sampleRate", {
get: () => 48000, // Common hardware rate
configurable: true,
enumerable: true,
});
defineAudioGetter(AudioContext.prototype, "sampleRate");
}
@@ -1,46 +1,51 @@
// Define a consistent locale profile
const locale = "en-US";
const languages = ["en-US", "en"];
const timezone = "America/New_York";
const languages = [locale, "en"];
// Override navigator language properties
Object_defineProperty(navigator, "language", {
get: () => locale,
configurable: true,
enumerable: true,
});
Object_defineProperty(navigator, "languages", {
get: () => [...languages],
configurable: true,
enumerable: true,
});
// Override Intl.DateTimeFormat for timezone consistency
const OriginalDateTimeFormat = Intl_DateTimeFormat;
Intl.DateTimeFormat = class extends OriginalDateTimeFormat {
constructor(locales, options) {
const mergedOptions = { ...options, timeZone: timezone };
super(locales, mergedOptions);
const sameLanguages = (value) => {
if (!value || value.length !== languages.length) return false;
for (let index = 0; index < languages.length; index += 1) {
if (value[index] !== languages[index]) return false;
}
resolvedOptions() {
const options = super.resolvedOptions();
options.timeZone = timezone;
return options;
return true;
};
const navigatorProto = Object_getPrototypeOf(navigator);
const navigatorAccessors = {
get language() {
return locale;
},
get languages() {
return [locale, "en"];
},
};
const defineNavigatorAccessor = (name) => {
const owner = navigatorProto || navigator;
const descriptor =
(navigatorProto && Object_getOwnPropertyDescriptor(navigatorProto, name)) ||
Object_getOwnPropertyDescriptor(navigator, name);
if (descriptor && descriptor.configurable === false) return;
const accessor = Object_getOwnPropertyDescriptor(navigatorAccessors, name);
if (!accessor || !accessor.get) return;
patchToString(accessor.get, "get " + name);
Object_defineProperty(owner, name, {
get: accessor.get,
configurable: descriptor ? descriptor.configurable : true,
enumerable: descriptor ? descriptor.enumerable : true,
});
const ownDescriptor = Object_getOwnPropertyDescriptor(navigator, name);
if (owner !== navigator && ownDescriptor && ownDescriptor.configurable !== false) {
Reflect_deleteProperty(navigator, name);
}
};
// Ensure Date timezone is consistent
const originalDateConstructor = Date_constructor;
const originalToString = originalDateConstructor.prototype.toString;
const originalToTimeString = originalDateConstructor.prototype.toTimeString;
if (navigator.language !== locale) {
defineNavigatorAccessor("language");
}
Date.prototype.toString = function () {
return originalToString
.call(this)
.replace(/\(.*\)$/, "(Eastern Standard Time)");
};
Date.prototype.toTimeString = function () {
return originalToTimeString
.call(this)
.replace(/\(.*\)$/, "(Eastern Standard Time)");
};
if (!sameLanguages(navigator.languages)) {
defineNavigatorAccessor("languages");
}
@@ -54,8 +54,8 @@ const defineProp = (obj, prop, value) =>
configurable: true,
});
const generateFunctionMocks = (proto, itemMainProp, dataArray) => ({
item: new Window_Proxy(proto.item, {
const generateFunctionMocks = (proto, itemMainProp, dataArray) => {
const item = new Window_Proxy(proto.item, {
apply(target, ctx, args) {
if (!args.length) {
throw new TypeError(
@@ -65,8 +65,8 @@ const generateFunctionMocks = (proto, itemMainProp, dataArray) => ({
const isInteger = args[0] && Number.isInteger(Number(args[0]));
return (isInteger ? dataArray[Number(args[0])] : dataArray[0]) || null;
},
}),
namedItem: new Window_Proxy(proto.namedItem, {
});
const namedItem = new Window_Proxy(proto.namedItem, {
apply(target, ctx, args) {
if (!args.length) {
throw new TypeError(
@@ -75,15 +75,19 @@ const generateFunctionMocks = (proto, itemMainProp, dataArray) => ({
}
return dataArray.find(item => item[itemMainProp] === args[0]) || null;
},
}),
refresh: proto.refresh
});
patchToString(item, "item");
patchToString(namedItem, "namedItem");
const refresh = proto.refresh
? new Window_Proxy(proto.refresh, {
apply() {
return undefined;
},
})
: undefined,
});
: undefined;
patchToString(refresh, "refresh");
return { item, namedItem, refresh };
};
const generateMagicArray = (dataArray, proto, itemProto, itemMainProp) => {
const makeItem = (data) => {
@@ -1,8 +1,59 @@
const navigatorProto = Object_getPrototypeOf(navigator);
if (navigatorProto && "hardwareConcurrency" in navigatorProto) {
Object_defineProperty(navigatorProto, "hardwareConcurrency", {
get: () => 4,
configurable: true,
enumerable: true,
const hardwareConcurrencyName = "hardwareConcurrency";
let hardwareConcurrencyProto = Object_getPrototypeOf(navigator);
let hardwareConcurrencyOwner;
let hardwareConcurrencyDescriptor;
while (hardwareConcurrencyProto && !hardwareConcurrencyDescriptor) {
hardwareConcurrencyDescriptor = Object_getOwnPropertyDescriptor(
hardwareConcurrencyProto,
hardwareConcurrencyName,
);
if (hardwareConcurrencyDescriptor) {
hardwareConcurrencyOwner = hardwareConcurrencyProto;
} else {
hardwareConcurrencyProto = Object_getPrototypeOf(hardwareConcurrencyProto);
}
}
const hardwareConcurrencyValue = 8;
let shouldPatchHardwareConcurrency = false;
if (
hardwareConcurrencyOwner &&
hardwareConcurrencyDescriptor &&
hardwareConcurrencyDescriptor.configurable &&
typeof hardwareConcurrencyDescriptor.get === "function"
) {
shouldPatchHardwareConcurrency = true;
try {
shouldPatchHardwareConcurrency =
Reflect_apply(hardwareConcurrencyDescriptor.get, navigator, []) !==
hardwareConcurrencyValue;
} catch {
shouldPatchHardwareConcurrency = false;
}
}
if (shouldPatchHardwareConcurrency) {
const hardwareConcurrencyAccessors = {
get hardwareConcurrency() {
Reflect_apply(hardwareConcurrencyDescriptor.get, this, []);
return hardwareConcurrencyValue;
},
};
const getHardwareConcurrency = Object_getOwnPropertyDescriptor(
hardwareConcurrencyAccessors,
hardwareConcurrencyName,
).get;
if (typeof patchToString === "function") {
patchToString(getHardwareConcurrency, "get hardwareConcurrency");
}
Object_defineProperty(hardwareConcurrencyOwner, hardwareConcurrencyName, {
get: getHardwareConcurrency,
set: hardwareConcurrencyDescriptor.set,
enumerable: hardwareConcurrencyDescriptor.enumerable,
configurable: hardwareConcurrencyDescriptor.configurable,
});
}
@@ -15,25 +15,27 @@ const parseInput = (arg) => {
};
const originalCanPlayType = HTMLMediaElement.prototype.canPlayType;
const proxiedCanPlayType = new Window_Proxy(originalCanPlayType, {
apply(target, ctx, args) {
if (!args || !args.length) {
return Reflect_apply(target, ctx, args);
}
const { mime, codecs } = parseInput(args[0]);
if (mime === "video/mp4" && codecs.includes("avc1.42E01E")) {
return "probably";
}
if (mime === "audio/x-m4a" && !codecs.length) {
return "maybe";
}
if (mime === "audio/aac" && !codecs.length) {
return "probably";
}
return Reflect_apply(target, ctx, args);
},
});
patchToString(proxiedCanPlayType, "canPlayType");
Object_defineProperty(HTMLMediaElement.prototype, "canPlayType", {
value: new Window_Proxy(originalCanPlayType, {
apply(target, ctx, args) {
if (!args || !args.length) {
return Reflect_apply(target, ctx, args);
}
const { mime, codecs } = parseInput(args[0]);
if (mime === "video/mp4" && codecs.includes("avc1.42E01E")) {
return "probably";
}
if (mime === "audio/x-m4a" && !codecs.length) {
return "maybe";
}
if (mime === "audio/aac" && !codecs.length) {
return "probably";
}
return Reflect_apply(target, ctx, args);
},
}),
value: proxiedCanPlayType,
writable: true,
configurable: true,
enumerable: true,
@@ -1,52 +1,214 @@
const patchWorkerConstructor = (name, OriginalWorker) => {
if (typeof OriginalWorker !== "function") return;
const buildWrappedUrl = (scriptURL, options) => {
const ua = navigator.userAgent;
const platform = navigator.platform;
const uaData = navigator.userAgentData && typeof navigator.userAgentData.toJSON === "function"
? navigator.userAgentData.toJSON()
: navigator.userAgentData;
const windowDescriptor = Object_getOwnPropertyDescriptor(window, name);
if (windowDescriptor && windowDescriptor.configurable === false) return;
const preludeLines = [
"try {",
`const ua = ${JSON.stringify(ua)};`,
`const platform = ${JSON.stringify(platform)};`,
"Object_defineProperty(self.navigator, 'userAgent', { get: () => ua, configurable: true });",
"Object_defineProperty(self.navigator, 'platform', { get: () => platform, configurable: true });",
];
const NativeURL = window.URL;
const URL_createObjectURL = NativeURL && NativeURL.createObjectURL;
const URL_revokeObjectURL = NativeURL && NativeURL.revokeObjectURL;
if (
typeof NativeURL !== "function" ||
typeof URL_createObjectURL !== "function" ||
typeof URL_revokeObjectURL !== "function"
) {
return;
}
if (uaData) {
preludeLines.push(`const uaData = ${JSON.stringify(uaData)};`);
preludeLines.push(
"Object_defineProperty(self.navigator, 'userAgentData', { get: () => uaData, configurable: true });",
);
const sharedWorkerUrls = name === "SharedWorker" ? new Map() : undefined;
const revokeUrl = (url) => {
try {
Reflect_apply(URL_revokeObjectURL, NativeURL, [url]);
} catch {}
};
const scheduleWorkerUrlRevoke = (worker, url) => {
let revoked = false;
const revokeOnce = () => {
if (revoked) return;
revoked = true;
revokeUrl(url);
};
try {
if (typeof worker.addEventListener === "function") {
Reflect_apply(worker.addEventListener, worker, ["error", revokeOnce, { once: true }]);
}
} catch {}
Window_setTimeout(revokeOnce, 1000);
};
if (sharedWorkerUrls) {
try {
window.addEventListener("pagehide", (event) => {
if (event && event.persisted) return;
for (const url of sharedWorkerUrls.values()) {
revokeUrl(url);
}
sharedWorkerUrls.clear();
});
} catch {}
}
const canWrapArguments = (args) => {
if (!args || args.length !== 1) {
return name === "SharedWorker" && args && args.length === 2 && typeof args[1] === "string";
}
return true;
};
const resolveWorkerUrl = (scriptURL) => {
const baseUrl = document.baseURI || window.location.href;
const scriptUrlString =
typeof scriptURL === "string"
? scriptURL
: scriptURL instanceof NativeURL
? scriptURL.href
: undefined;
if (scriptUrlString === undefined) return undefined;
let absoluteUrl;
try {
absoluteUrl = new NativeURL(scriptUrlString, baseUrl);
} catch {
return undefined;
}
if (absoluteUrl.origin !== window.location.origin) return undefined;
if (absoluteUrl.protocol !== "http:" && absoluteUrl.protocol !== "https:") return undefined;
if (absoluteUrl.username || absoluteUrl.password) return undefined;
return absoluteUrl.href;
};
const buildWorkerPrelude = () => {
const values = [];
const addNavigatorString = (prop) => {
try {
const value = navigator[prop];
if (typeof value === "string") values.push([prop, value]);
} catch {}
};
addNavigatorString("userAgent");
addNavigatorString("platform");
if (!values.length) return "";
return `(() => {
try {
const values = ${JSON.stringify(values)};
const nav = self.navigator;
if (!nav) return;
const defineProperty = Object.defineProperty;
const getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
const getPrototypeOf = Object.getPrototypeOf;
const Reflect_apply = Reflect.apply;
const nativeFunctionToString = Function.prototype.toString;
const nativeSources = new WeakMap();
const WeakMap_get = WeakMap.prototype.get;
const WeakMap_has = WeakMap.prototype.has;
const WeakMap_set = WeakMap.prototype.set;
const rememberNative = (fn, source) => {
if (typeof fn === "function") Reflect_apply(WeakMap_set, nativeSources, [fn, source]);
return fn;
};
const findDescriptor = (prop) => {
let owner = nav;
while (owner) {
const descriptor = getOwnPropertyDescriptor(owner, prop);
if (descriptor) return [owner, descriptor];
owner = getPrototypeOf(owner);
}
return [getPrototypeOf(nav) || nav, undefined];
};
let patched = false;
for (let i = 0; i < values.length; i += 1) {
const prop = values[i][0];
const value = values[i][1];
try {
if (nav[prop] === value) continue;
} catch (_) {}
const found = findDescriptor(prop);
const owner = found[0];
const descriptor = found[1];
if (!owner || (descriptor && descriptor.configurable === false)) continue;
const getterDescriptor = getOwnPropertyDescriptor({ get [prop]() { return value; } }, prop);
const getter = getterDescriptor && getterDescriptor.get;
if (typeof getter !== "function") continue;
rememberNative(getter, "function get " + prop + "() { [native code] }");
defineProperty(owner, prop, {
get: getter,
enumerable: descriptor ? descriptor.enumerable : true,
configurable: true,
});
patched = true;
}
if (!patched) return;
const toStringDescriptor = getOwnPropertyDescriptor(Function.prototype, "toString");
if (toStringDescriptor && toStringDescriptor.configurable === false) return;
const functionToString = new Proxy(nativeFunctionToString, {
apply(target, thisArg, args) {
if (Reflect_apply(WeakMap_has, nativeSources, [thisArg])) return Reflect_apply(WeakMap_get, nativeSources, [thisArg]);
return Reflect_apply(target, thisArg, args);
},
});
rememberNative(functionToString, "function toString() { [native code] }");
defineProperty(Function.prototype, "toString", {
value: functionToString,
writable: toStringDescriptor ? toStringDescriptor.writable : true,
configurable: toStringDescriptor ? toStringDescriptor.configurable : true,
enumerable: toStringDescriptor ? toStringDescriptor.enumerable : false,
});
} catch (_) {}
})();`;
};
const buildWrappedUrl = (scriptURL) => {
const originalUrl = resolveWorkerUrl(scriptURL);
if (!originalUrl) return undefined;
if (sharedWorkerUrls) {
const cachedUrl = sharedWorkerUrls.get(originalUrl);
if (cachedUrl) return { url: cachedUrl, cacheKey: originalUrl };
}
preludeLines.push("} catch (e) {};");
const prelude = preludeLines.join("\n");
const importLine = options?.type === "module"
? `import ${JSON.stringify(String(scriptURL))};`
: `importScripts(${JSON.stringify(String(scriptURL))});`;
const blob = new Window_Blob([prelude, "\n", importLine], { type: "application/javascript" });
const url = URL.createObjectURL(blob);
return url;
const prelude = buildWorkerPrelude();
if (!prelude) return undefined;
try {
const blob = new Window_Blob(
[prelude, "\n", `importScripts(${JSON.stringify(originalUrl)});`],
{ type: "application/javascript" },
);
const url = Reflect_apply(URL_createObjectURL, NativeURL, [blob]);
if (sharedWorkerUrls) sharedWorkerUrls.set(originalUrl, url);
return { url, cacheKey: sharedWorkerUrls ? originalUrl : undefined };
} catch {
return undefined;
}
};
const handler = {
construct(target, args) {
construct(target, args, newTarget) {
if (!canWrapArguments(args)) {
return Reflect_construct(target, args || [], newTarget);
}
const wrapped = buildWrappedUrl(args[0]);
if (!wrapped) {
return Reflect_construct(target, args || [], newTarget);
}
const wrappedArgs = [wrapped.url];
for (let i = 1; i < args.length; i += 1) {
wrappedArgs[i] = args[i];
}
try {
const scriptURL = args?.[0];
const options = args?.[1];
if (!scriptURL) {
return new target(...(args || []));
const worker = Reflect_construct(target, wrappedArgs, newTarget);
if (!sharedWorkerUrls) {
scheduleWorkerUrlRevoke(worker, wrapped.url);
}
const wrappedUrl = buildWrappedUrl(scriptURL, options);
const worker = new target(wrappedUrl, options);
URL.revokeObjectURL(wrappedUrl);
return worker;
} catch {
return new target(...(args || []));
if (wrapped.cacheKey && sharedWorkerUrls) sharedWorkerUrls.delete(wrapped.cacheKey);
revokeUrl(wrapped.url);
return Reflect_construct(target, args || [], newTarget);
}
},
apply(target, thisArg, args) {
@@ -55,16 +217,15 @@ const patchWorkerConstructor = (name, OriginalWorker) => {
};
const proxied = new Window_Proxy(OriginalWorker, handler);
patchToString(proxied, name);
Object_defineProperty(window, name, {
value: proxied,
writable: true,
configurable: true,
});
Object_defineProperty(window[name], "toString", {
value: Function_toString.bind(OriginalWorker),
writable: false,
configurable: true,
enumerable: false,
writable:
windowDescriptor && "writable" in windowDescriptor
? windowDescriptor.writable
: true,
configurable: windowDescriptor ? windowDescriptor.configurable : true,
enumerable: windowDescriptor ? windowDescriptor.enumerable : false,
});
};
+648
View File
@@ -0,0 +1,648 @@
diff --git a/lib/puppeteer/cdp/ExecutionContext.js b/lib/puppeteer/cdp/ExecutionContext.js
index d2cfb72b63070527185c8d2e3b9bf7b19e91baa4..439821d543413ea620833ebe94908fe44e70b836 100644
--- a/lib/puppeteer/cdp/ExecutionContext.js
+++ b/lib/puppeteer/cdp/ExecutionContext.js
@@ -326,14 +326,19 @@ export class ExecutionContext extends EventEmitter {
return await this.#evaluate(false, pageFunction, ...args);
}
async #evaluate(returnByValue, pageFunction, ...args) {
- const sourceUrlComment = getSourceUrlComment(getSourcePuppeteerURLIfAvailable(pageFunction)?.toString() ??
- PuppeteerURL.INTERNAL_URL);
+ // xxx-stealth: never append the synthetic
+ // `//# sourceURL=__puppeteer_evaluation_script__` marker. That constant string
+ // leaks automation through V8 error stacks / debugger script listings. Any
+ // genuine user-supplied sourceURL already in the source is preserved untouched.
+ void getSourceUrlComment;
+ void PuppeteerURL;
+ const sourceUrlComment = '';
if (isString(pageFunction)) {
const contextId = this.#id;
const expression = pageFunction;
const expressionWithSourceUrl = SOURCE_URL_REGEX.test(expression)
? expression
- : `${expression}\n${sourceUrlComment}\n`;
+ : expression;
const { exceptionDetails, result: remoteObject } = await this.#client
.send('Runtime.evaluate', {
expression: expressionWithSourceUrl,
@@ -352,9 +357,8 @@ export class ExecutionContext extends EventEmitter {
return this.#world.createCdpHandle(remoteObject);
}
const functionDeclaration = stringifyFunction(pageFunction);
- const functionDeclarationWithSourceUrl = SOURCE_URL_REGEX.test(functionDeclaration)
- ? functionDeclaration
- : `${functionDeclaration}\n${sourceUrlComment}\n`;
+ void sourceUrlComment;
+ const functionDeclarationWithSourceUrl = functionDeclaration;
let callFunctionOnPromise;
try {
callFunctionOnPromise = this.#client.send('Runtime.callFunctionOn', {
diff --git a/lib/puppeteer/cdp/FrameManager.js b/lib/puppeteer/cdp/FrameManager.js
index 23afcd3def20ff2f578bf14692de1c86365e95c8..26925d30746cb83d0322ea8b4e93993ff4a28a8e 100644
--- a/lib/puppeteer/cdp/FrameManager.js
+++ b/lib/puppeteer/cdp/FrameManager.js
@@ -13,6 +13,7 @@ import { disposeSymbol } from '../util/disposable.js';
import { isErrorLike } from '../util/ErrorLike.js';
import { CdpIssue } from './CdpIssue.js';
import { CdpPreloadScript } from './CdpPreloadScript.js';
+import { CDP_BINDING_PREFIX } from './utils.js';
import { isTargetClosedError } from './Connection.js';
import { CdpDeviceRequestPromptManager } from './DeviceRequestPrompt.js';
import { ExecutionContext } from './ExecutionContext.js';
@@ -44,6 +45,10 @@ export class FrameManager extends EventEmitter {
* frameNavigated event usually contains the latest information.
*/
#frameNavigatedReceived = new Set();
+ // xxx-stealth: coalesce concurrent world re-acquisitions per frame so the
+ // frameNavigated/init/load triggers don't stomp each other's contexts.
+ #acquireQueued = new Set();
+ #acquirePromises = new Map();
#deviceRequestPromptManagerMap = new WeakMap();
#frameTreeHandled;
get timeoutSettings() {
@@ -181,9 +186,18 @@ export class FrameManager extends EventEmitter {
this.#frameTreeHandled?.resolve();
}),
client.send('Page.setLifecycleEventsEnabled', { enabled: true }),
- client.send('Runtime.enable').then(() => {
- return this.#createIsolatedWorld(client, UTILITY_WORLD_NAME);
- }),
+ // xxx-stealth: do NOT send Runtime.enable. It is the single
+ // most-detected automation tell (Brotector/CreepJS/Cloudflare probe
+ // it). Execution contexts are instead acquired pull-style in
+ // #acquireWorlds (main world via Runtime.evaluate globalThis idOnly,
+ // utility world via the Page.createIsolatedWorld response) and fed
+ // into the existing push pipeline via #onExecutionContextCreated.
+ // The utility-world preload sentinel is kept so world-scoped init
+ // scripts still attach on navigation.
+ client.send('Page.addScriptToEvaluateOnNewDocument', {
+ source: `//# sourceURL=${PuppeteerURL.INTERNAL_URL}`,
+ worldName: UTILITY_WORLD_NAME,
+ }).catch(debugError),
...(frame
? Array.from(this.#scriptsToEvaluateOnNewDocument.values())
: []).map(script => {
@@ -336,6 +350,7 @@ export class FrameManager extends EventEmitter {
return;
}
frame = new CdpFrame(this, frameId, parentFrameId, session);
+ this.#installContextProviders(frame);
this._frameTree.addFrame(frame);
this.emit(FrameManagerEvent.FrameAttached, frame);
}
@@ -366,6 +381,177 @@ export class FrameManager extends EventEmitter {
frame._navigated(framePayload);
this.emit(FrameManagerEvent.FrameNavigated, frame);
frame.emit(FrameEvent.FrameNavigated, navigationType);
+ // xxx-stealth: install lazy context providers and invalidate the
+ // pre-navigation contexts. With Runtime.enable off there is no
+ // executionContextDestroyed event, so dispose synchronously here; this makes
+ // IsolatedWorld.#context undefined so the next evaluate pulls a fresh context
+ // via its provider (resolved after the navigation has settled) instead of
+ // using a dead one. We intentionally do NOT proactively acquire — proactive
+ // contexts captured mid-navigation go stale silently. Resolution is lazy.
+ this.#installContextProviders(frame);
+ for (const world of this.#frameWorlds(frame)) {
+ world?.context?.[disposeSymbol]();
+ }
+ }
+ // xxx-stealth: the main + utility worlds. worlds is keyed by Symbols, so
+ // Object.values misses them — enumerate the known world symbols explicitly.
+ #frameWorlds(frame) {
+ return [frame.worlds[MAIN_WORLD], frame.worlds[PUPPETEER_WORLD]];
+ }
+ // xxx-stealth: point each of the frame's worlds at the coalesced acquirer
+ // so IsolatedWorld can pull its context on demand.
+ #installContextProviders(frame) {
+ for (const world of this.#frameWorlds(frame)) {
+ world?.setContextProvider?.(() => this.#acquireWorlds(frame));
+ }
+ }
+ // xxx-stealth: coalescing acquirer. Returns a promise that resolves when
+ // the current (or freshly started) acquisition for this frame completes, so a
+ // lazy provider can await it. Concurrent callers share the in-flight promise
+ // rather than racing — concurrent acquires resolve different transient contexts
+ // and blank each other.
+ #acquireWorlds(frame) {
+ const id = frame._id;
+ const existing = this.#acquirePromises.get(id);
+ if (existing) {
+ this.#acquireQueued.add(id);
+ return existing;
+ }
+ const promise = this.#doAcquireWorlds(frame).finally(() => {
+ this.#acquirePromises.delete(id);
+ if (this.#acquireQueued.delete(id) && this.frame(id)) {
+ void this.#acquireWorlds(frame);
+ }
+ });
+ this.#acquirePromises.set(id, promise);
+ return promise;
+ }
+ // xxx-stealth: true when `frame` is the top frame of its CDP session
+ // (the page main frame, or an OOP iframe root). Only such frames can resolve
+ // their main world via a context-less Runtime.evaluate, because that targets
+ // the session's default context. Same-process sub-frames share the parent's
+ // session, so a context-less evaluate would resolve the WRONG frame — we skip
+ // proactive main-world acquisition for them rather than mis-register.
+ #frameIsTopOfSession(frame) {
+ const parentId = frame._parentId;
+ if (!parentId) {
+ return true;
+ }
+ const parent = this.frame(parentId);
+ return !parent || parent.client !== frame.client;
+ }
+ // xxx-stealth: pull-acquire a frame's main + utility execution contexts
+ // without Runtime.enable, then feed them into the normal push pipeline.
+ async #doAcquireWorlds(frame) {
+ const session = frame.client;
+ // xxx-stealth: never pre-dispose here. IsolatedWorld.setContext
+ // already disposes the previous context when a fresh one is installed, and
+ // a transiently-failed resolve (common while a navigation is mid-flight)
+ // must leave the last good context intact rather than blank the world.
+ // Stale invalidation on navigation is handled once in #onFrameNavigated.
+ try {
+ // Utility (PUPPETEER) world: Page.createIsolatedWorld returns the new
+ // context id directly — works for any frameId on the session.
+ const iso = await session
+ .send('Page.createIsolatedWorld', {
+ frameId: frame._id,
+ worldName: UTILITY_WORLD_NAME,
+ grantUniveralAccess: true,
+ })
+ .catch(debugError);
+ const utilityId = iso && typeof iso.executionContextId === 'number' ? iso.executionContextId : undefined;
+ if (utilityId !== undefined) {
+ this.#onExecutionContextCreated({
+ id: utilityId,
+ name: UTILITY_WORLD_NAME,
+ origin: '',
+ auxData: { frameId: frame._id, isDefault: false },
+ }, session);
+ }
+ // Main world: resolve this frame's main execution context id.
+ const id = await this.#resolveMainContextId(session, frame, utilityId);
+ if (id !== undefined) {
+ this.#onExecutionContextCreated({
+ id,
+ name: '',
+ origin: '',
+ auxData: { frameId: frame._id, isDefault: true },
+ }, session);
+ // xxx-stealth: re-install exposed-function bindings into the
+ // freshly acquired main world. Normally the binding wrapper is
+ // (re)installed when Chrome fires executionContextCreated; with that
+ // event silenced we must re-add the native binding for this context
+ // id and re-run the wrapper init source ourselves on every navigation.
+ for (const binding of this.#bindings) {
+ void session
+ .send('Runtime.addBinding', {
+ name: CDP_BINDING_PREFIX + binding.name,
+ executionContextId: id,
+ })
+ .catch(() => { });
+ void session
+ .send('Runtime.evaluate', {
+ expression: binding.initSource,
+ contextId: id,
+ })
+ .catch(() => { });
+ }
+ }
+ }
+ catch (error) {
+ debugError(error);
+ }
+ }
+ // xxx-stealth: resolve a frame's MAIN-world execution context id without
+ // Runtime.enable. For the top frame of a session a context-less
+ // `Runtime.evaluate globalThis` resolves the session default (cheap, 1 RTT). For
+ // same-process sub-frames that would resolve the PARENT, so instead we take the
+ // frame's document node (via the utility world we just created) and DOM.resolveNode
+ // it with no executionContextId — CDP resolves it in the owning frame's main world,
+ // whose objectId encodes the main context id. The objectId format is
+ // `<backend>.<contextId>.<n>`.
+ async #resolveMainContextId(session, frame, utilityId) {
+ const parse = (objectId) => {
+ if (typeof objectId !== 'string') {
+ return undefined;
+ }
+ const id = Number.parseInt(objectId.split('.')[1] ?? '', 10);
+ return Number.isNaN(id) ? undefined : id;
+ };
+ if (this.#frameIsTopOfSession(frame)) {
+ const globalThis = await session
+ .send('Runtime.evaluate', {
+ expression: 'globalThis',
+ serializationOptions: { serialization: 'idOnly' },
+ })
+ .catch(debugError);
+ return parse(globalThis?.result?.objectId);
+ }
+ if (utilityId === undefined) {
+ return undefined;
+ }
+ const utilDoc = await session
+ .send('Runtime.evaluate', {
+ expression: 'document',
+ contextId: utilityId,
+ serializationOptions: { serialization: 'idOnly' },
+ })
+ .catch(debugError);
+ const utilDocObjectId = utilDoc?.result?.objectId;
+ if (typeof utilDocObjectId !== 'string') {
+ return undefined;
+ }
+ const described = await session
+ .send('DOM.describeNode', { objectId: utilDocObjectId })
+ .catch(debugError);
+ const backendNodeId = described?.node?.backendNodeId;
+ if (typeof backendNodeId !== 'number') {
+ return undefined;
+ }
+ const mainNode = await session
+ .send('DOM.resolveNode', { backendNodeId })
+ .catch(debugError);
+ return parse(mainNode?.object?.objectId);
}
async #createIsolatedWorld(session, name) {
const key = `${session.id()}:${name}`;
diff --git a/lib/puppeteer/cdp/IsolatedWorld.js b/lib/puppeteer/cdp/IsolatedWorld.js
index b0619734a9eeb884f3ac4ffff39b540226aaf818..0ef7ce4616a27ff9aabd0747937b593f28bb36de 100644
--- a/lib/puppeteer/cdp/IsolatedWorld.js
+++ b/lib/puppeteer/cdp/IsolatedWorld.js
@@ -21,6 +21,13 @@ export class IsolatedWorld extends Realm {
#worldId;
#origin;
#frameOrWorker;
+ // xxx-stealth: lazy context provider. With Runtime.enable disabled there
+ // are no executionContextCreated events to push contexts, so the world resolves
+ // its context on demand the first time it is needed after a navigation.
+ #contextProvider;
+ setContextProvider(provider) {
+ this.#contextProvider = provider;
+ }
constructor(frameOrWorker, timeoutSettings, worldId) {
super(timeoutSettings);
this.#frameOrWorker = frameOrWorker;
@@ -72,6 +79,19 @@ export class IsolatedWorld extends Realm {
* Waits for the next context to be set on the isolated world.
*/
async #waitForExecutionContext() {
+ // xxx-stealth: pull the context on demand before falling back to
+ // waiting for a (never-arriving) push event.
+ if (this.#contextProvider && !this.#context && !this.disposed) {
+ try {
+ await this.#contextProvider();
+ }
+ catch {
+ // fall through to the event wait below
+ }
+ if (this.#context) {
+ return this.#context;
+ }
+ }
const error = new Error('Execution context was destroyed');
const result = await firstValueFrom(fromEmitterEvent(this.#emitter, 'context').pipe(raceWith(fromEmitterEvent(this.#emitter, 'disposed').pipe(map(() => {
// The message has to match the CDP message expected by the WaitTask class.
diff --git a/lib/puppeteer/cdp/WebWorker.js b/lib/puppeteer/cdp/WebWorker.js
index e3ee6673245e92faa53a028867e7dbb3a1aed089..4fcc9785077ca868fd0fa892b4ec907cd807ed96 100644
--- a/lib/puppeteer/cdp/WebWorker.js
+++ b/lib/puppeteer/cdp/WebWorker.js
@@ -27,9 +27,21 @@ export class CdpWebWorker extends WebWorker {
this.#targetType = targetType;
this.#world = new IsolatedWorld(this, new TimeoutSettings(), MAIN_WORLD);
this.#emitter = new EventEmitter();
- this.#client.once('Runtime.executionContextCreated', async (event) => {
- this.#world.setContext(new ExecutionContext(client, event.context, this.#world));
- });
+ // xxx-stealth: acquire the worker's execution context via an idOnly
+ // globalThis evaluate instead of Runtime.enable + executionContextCreated.
+ void this.#client
+ .send('Runtime.evaluate', {
+ expression: 'globalThis',
+ serializationOptions: { serialization: 'idOnly' },
+ })
+ .then((res) => {
+ const objectId = res?.result?.objectId;
+ const id = typeof objectId === 'string' ? Number.parseInt(objectId.split('.')[1] ?? '', 10) : NaN;
+ if (!Number.isNaN(id)) {
+ this.#world.setContext(new ExecutionContext(client, { id }, this.#world));
+ }
+ })
+ .catch(debugError);
this.#world.emitter.on('consoleapicalled', async (event) => {
try {
const values = event.args.map(arg => {
@@ -61,7 +73,6 @@ export class CdpWebWorker extends WebWorker {
});
// This might fail if the target is closed before we receive all execution contexts.
networkManager?.addClient(this.#client).catch(debugError);
- this.#client.send('Runtime.enable').catch(debugError);
}
mainRealm() {
return this.#world;
diff --git a/lib/puppeteer/node/ChromeLauncher.js b/lib/puppeteer/node/ChromeLauncher.js
index 94db7a76f81ecdcff59a620c1fd5b65957e22e68..fdd8de60c97b7c3037c0d7676fabf150e190751c 100644
--- a/lib/puppeteer/node/ChromeLauncher.js
+++ b/lib/puppeteer/node/ChromeLauncher.js
@@ -126,22 +126,12 @@ export class ChromeLauncher extends BrowserLauncher {
].filter(feature => {
return feature !== '';
});
- // Merge default disabled features with user-provided ones, if any.
+ // xxx-stealth: drop puppeteer's default --disable-features list. That
+ // list (Translate, AcceptCHFrame, MediaRouter, ...) is a non-default flag
+ // fingerprint vs a real user-launched Chrome. Only honor user-supplied
+ // disabled features so the assembled --disable-features looks organic.
+ void turnOnExperimentalFeaturesForTesting;
const disabledFeatures = [
- 'Translate',
- // AcceptCHFrame disabled because of crbug.com/1348106.
- 'AcceptCHFrame',
- 'MediaRouter',
- 'OptimizationHints',
- 'WebUIReloadButton',
- ...(turnOnExperimentalFeaturesForTesting
- ? []
- : [
- // https://crbug.com/1492053
- 'ProcessPerSiteUpToMainFrameThreshold',
- // https://github.com/puppeteer/puppeteer/issues/10715
- 'IsolateSandboxedIframes',
- ]),
...userDisabledFeatures,
]
.filter(feature => {
diff --git a/lib/puppeteer/api/Frame.js b/lib/puppeteer/api/Frame.js
--- a/lib/puppeteer/api/Frame.js
+++ b/lib/puppeteer/api/Frame.js
@@ -119,6 +119,31 @@
export const throwIfDetached = throwIfDisposed(frame => {
return `Attempted to use detached Frame '${frame._id}'.`;
});
+const MAIN_WORLD_DIRECTIVE = /^\s*(?:(?:\/\/!world=main(?=$|\s))|(?:\/\*!world=main\s*\*\/))/;
+const shouldEvaluateInMainWorld = (pageFunction) => {
+ if (typeof pageFunction !== 'function' && typeof pageFunction !== 'string') {
+ return false;
+ }
+ let source;
+ try {
+ source =
+ typeof pageFunction === 'string'
+ ? pageFunction
+ : Function.prototype.toString.call(pageFunction);
+ }
+ catch {
+ return false;
+ }
+ if (MAIN_WORLD_DIRECTIVE.test(source)) {
+ return true;
+ }
+ if (typeof pageFunction !== 'function') {
+ return false;
+ }
+ const arrowIndex = source.indexOf('=>');
+ const bodyStart = source.indexOf('{', arrowIndex >= 0 ? arrowIndex : 0);
+ return bodyStart >= 0 && MAIN_WORLD_DIRECTIVE.test(source.slice(bodyStart + 1));
+};
/**
* Represents a DOM frame.
*
@@ -277,12 +302,21 @@
super();
}
#_document;
+ #_mainDocument;
/**
* @internal
*/
- #document() {
+ #document(mainWorld = false) {
+ if (mainWorld) {
+ if (!this.#_mainDocument) {
+ this.#_mainDocument = this.mainRealm().evaluateHandle(() => {
+ return document;
+ });
+ }
+ return this.#_mainDocument;
+ }
if (!this.#_document) {
- this.#_document = this.mainRealm().evaluateHandle(() => {
+ this.#_document = this.isolatedRealm().evaluateHandle(() => {
return document;
});
}
@@ -295,6 +329,7 @@
*/
clearDocumentHandle() {
this.#_document = undefined;
+ this.#_mainDocument = undefined;
}
/**
* @returns The frame element associated with this frame (if any).
@@ -345,8 +380,9 @@
* See {@link Page.evaluateHandle} for details.
*/
async evaluateHandle(pageFunction, ...args) {
+ const realm = shouldEvaluateInMainWorld(pageFunction) ? this.mainRealm() : this.isolatedRealm();
pageFunction = withSourcePuppeteerURLIfNone(this.evaluateHandle.name, pageFunction);
- return await this.mainRealm().evaluateHandle(pageFunction, ...args);
+ return await realm.evaluateHandle(pageFunction, ...args);
}
/**
* Behaves identically to {@link Page.evaluate} except it's run within
@@ -355,8 +391,9 @@
* See {@link Page.evaluate} for details.
*/
async evaluate(pageFunction, ...args) {
+ const realm = shouldEvaluateInMainWorld(pageFunction) ? this.mainRealm() : this.isolatedRealm();
pageFunction = withSourcePuppeteerURLIfNone(this.evaluate.name, pageFunction);
- return await this.mainRealm().evaluate(pageFunction, ...args);
+ return await realm.evaluate(pageFunction, ...args);
}
/**
* @internal
@@ -458,9 +495,10 @@
* @returns A promise to the result of the function.
*/
async $eval(selector, pageFunction, ...args) {
+ const mainWorld = shouldEvaluateInMainWorld(pageFunction);
pageFunction = withSourcePuppeteerURLIfNone(this.$eval.name, pageFunction);
// eslint-disable-next-line @puppeteer/use-using -- This is cached.
- const document = await this.#document();
+ const document = await this.#document(mainWorld);
return await document.$eval(selector, pageFunction, ...args);
}
/**
@@ -498,9 +536,10 @@
* @returns A promise to the result of the function.
*/
async $$eval(selector, pageFunction, ...args) {
+ const mainWorld = shouldEvaluateInMainWorld(pageFunction);
pageFunction = withSourcePuppeteerURLIfNone(this.$$eval.name, pageFunction);
// eslint-disable-next-line @puppeteer/use-using -- This is cached.
- const document = await this.#document();
+ const document = await this.#document(mainWorld);
return await document.$$eval(selector, pageFunction, ...args);
}
/**
@@ -577,7 +616,8 @@
* @returns the promise which resolve when the `pageFunction` returns a truthy value.
*/
async waitForFunction(pageFunction, options = {}, ...args) {
- return await this.mainRealm().waitForFunction(pageFunction, options, ...args);
+ const realm = shouldEvaluateInMainWorld(pageFunction) ? this.mainRealm() : this.isolatedRealm();
+ return await realm.waitForFunction(pageFunction, options, ...args);
}
/**
* The full HTML contents of the frame, including the DOCTYPE.
diff --git a/lib/puppeteer/cdp/Frame.js b/lib/puppeteer/cdp/Frame.js
--- a/lib/puppeteer/cdp/Frame.js
+++ b/lib/puppeteer/cdp/Frame.js
@@ -276,7 +276,7 @@
this.#client.send('Runtime.addBinding', {
name: CDP_BINDING_PREFIX + binding.name,
}),
- this.evaluate(binding.initSource).catch(debugError),
+ this.mainRealm().evaluate(binding.initSource).catch(debugError),
]);
}
async removeExposedFunctionBinding(binding) {
@@ -289,7 +289,7 @@
this.#client.send('Runtime.removeBinding', {
name: CDP_BINDING_PREFIX + binding.name,
}),
- this.evaluate(name => {
+ this.mainRealm().evaluate(name => {
// Removes the dangling Puppeteer binding wrapper.
// @ts-expect-error: In a different context.
globalThis[name] = undefined;
diff --git a/lib/puppeteer/api/ElementHandle.js b/lib/puppeteer/api/ElementHandle.js
--- a/lib/puppeteer/api/ElementHandle.js
+++ b/lib/puppeteer/api/ElementHandle.js
@@ -102,6 +102,31 @@
import { _isElementHandle } from './ElementHandleSymbol.js';
import { JSHandle } from './JSHandle.js';
import { NodeLocator } from './locators/locators.js';
+const MAIN_WORLD_DIRECTIVE = /^\s*(?:(?:\/\/!world=main(?=$|\s))|(?:\/\*!world=main\s*\*\/))/;
+const shouldEvaluateInMainWorld = (pageFunction) => {
+ if (typeof pageFunction !== 'function' && typeof pageFunction !== 'string') {
+ return false;
+ }
+ let source;
+ try {
+ source =
+ typeof pageFunction === 'string'
+ ? pageFunction
+ : Function.prototype.toString.call(pageFunction);
+ }
+ catch {
+ return false;
+ }
+ if (MAIN_WORLD_DIRECTIVE.test(source)) {
+ return true;
+ }
+ if (typeof pageFunction !== 'function') {
+ return false;
+ }
+ const arrowIndex = source.indexOf('=>');
+ const bodyStart = source.indexOf('{', arrowIndex >= 0 ? arrowIndex : 0);
+ return bodyStart >= 0 && MAIN_WORLD_DIRECTIVE.test(source.slice(bodyStart + 1));
+};
/**
* A given method will have it's `this` replaced with an isolated version of
* `this` when decorated with this decorator.
@@ -299,6 +324,7 @@
* trying to adopt it multiple times
*/
isolatedHandle = __runInitializers(this, _instanceExtraInitializers);
+ mainHandle;
/**
* @internal
*/
@@ -335,19 +361,37 @@
async getProperties() {
return await this.handle.getProperties();
}
+ async #handleForPageFunction(pageFunction) {
+ const realm = shouldEvaluateInMainWorld(pageFunction) ? this.frame.mainRealm() : this.frame.isolatedRealm();
+ if (this.realm === realm) {
+ return this;
+ }
+ if (realm === this.frame.isolatedRealm()) {
+ if (!this.isolatedHandle) {
+ this.isolatedHandle = await realm.adoptHandle(this);
+ }
+ return this.isolatedHandle;
+ }
+ if (!this.mainHandle) {
+ this.mainHandle = await realm.adoptHandle(this);
+ }
+ return this.mainHandle;
+ }
/**
* @internal
*/
async evaluate(pageFunction, ...args) {
+ const handle = await this.#handleForPageFunction(pageFunction);
pageFunction = withSourcePuppeteerURLIfNone(this.evaluate.name, pageFunction);
- return await this.handle.evaluate(pageFunction, ...args);
+ return await handle.handle.evaluate(pageFunction, ...args);
}
/**
* @internal
*/
async evaluateHandle(pageFunction, ...args) {
+ const handle = await this.#handleForPageFunction(pageFunction);
pageFunction = withSourcePuppeteerURLIfNone(this.evaluateHandle.name, pageFunction);
- return await this.handle.evaluateHandle(pageFunction, ...args);
+ return await handle.handle.evaluateHandle(pageFunction, ...args);
}
/**
* @internal
@@ -371,7 +415,7 @@
* @internal
*/
async dispose() {
- await Promise.all([this.handle.dispose(), this.isolatedHandle?.dispose()]);
+ await Promise.all([this.handle.dispose(), this.isolatedHandle?.dispose(), this.mainHandle?.dispose()]);
}
/**
* @internal
diff --git a/lib/puppeteer/api/ElementHandle.js b/lib/puppeteer/api/ElementHandle.js
--- a/lib/puppeteer/api/ElementHandle.js
+++ b/lib/puppeteer/api/ElementHandle.js
@@ -599,15 +599,27 @@
async $$eval(selector, pageFunction, ...args) {
const env_2 = { stack: [], error: void 0, hasError: false };
try {
+ const mainWorld = shouldEvaluateInMainWorld(pageFunction);
pageFunction = withSourcePuppeteerURLIfNone(this.$$eval.name, pageFunction);
const results = await this.$$(selector);
- const elements = __addDisposableResource(env_2, await this.evaluateHandle((_, ...elements) => {
+ const realm = mainWorld ? this.frame.mainRealm() : this.frame.isolatedRealm();
+ const adoptedResults = [];
+ const handlesToDispose = [];
+ for (const result of results) {
+ handlesToDispose.push(result);
+ const adopted = result.realm === realm ? result : await realm.adoptHandle(result);
+ adoptedResults.push(adopted);
+ if (adopted !== result) {
+ handlesToDispose.push(adopted);
+ }
+ }
+ const elements = __addDisposableResource(env_2, await realm.evaluateHandle((...elements) => {
return elements;
- }, ...results), false);
+ }, ...adoptedResults), false);
const [result] = await Promise.all([
elements.evaluate(pageFunction, ...args),
- ...results.map(results => {
- return results.dispose();
+ ...handlesToDispose.map(result => {
+ return result.dispose();
}),
]);
return result;