Files
openserp/core/browser/patch.js

274 lines
12 KiB
JavaScript

// Stealth patches injected via EvalOnNewDocument.
// Arguments are injected as a leading const block by the Go caller:
// const __langs = [...]; // navigator_langs from profile
// const __w = 1920; // viewport/content width
// const __h = 955; // viewport/content height
// const __screenW = 1920; // screen.width
// const __screenH = 1080; // screen.height
// const __availW = 1920; // screen.availWidth
// const __availH = 1040; // screen.availHeight
// const __availTop = 0; // screen.availTop
// const __outerW = 1920; // window.outerWidth
// const __outerH = 1040; // window.outerHeight
// const __webglVendor = "..."; // UNMASKED_VENDOR_WEBGL spoof
// const __webglRenderer = "..."; // UNMASKED_RENDERER_WEBGL spoof
//
// Scope: only patches that fix detectors with high ROI and low introspection
// surface. Notably absent: Function.prototype.toString proxy, custom Worker
// constructor, mass plugin/mimeType arrays, iframe contentWindow patching.
// Those triggered server-side detection on Google in commit 612e0dc.
(() => {
'use strict';
const sealGetter = (target, prop, fn) => {
try {
Object.defineProperty(target, prop, {
get: fn,
set: undefined,
enumerable: true,
configurable: true,
});
Object.defineProperty(target, prop, { configurable: false });
} catch (_) {}
};
// --- navigator.webdriver ---
// headless Chrome sets this to true. Delete it so getter returns undefined.
// (We rely on `disable-blink-features=AutomationControlled` already turning
// this off via the launcher; this is belt-and-braces in case it leaks.)
try {
if (typeof navigator.webdriver !== 'undefined') {
delete Object.getPrototypeOf(navigator).webdriver;
}
} catch (_) {}
// --- navigator.language / navigator.languages ---
// CDP setUserAgentOverride sets the HTTP header but not these JS props.
const primary = __langs.length ? __langs[0] : 'en-US';
const patchLangs = (target) => {
if (!target) return;
sealGetter(target, 'language', () => primary);
sealGetter(target, 'languages', () => Object.freeze(__langs.slice()));
};
patchLangs(Object.getPrototypeOf(navigator));
if (typeof WorkerNavigator !== 'undefined') {
patchLangs(WorkerNavigator.prototype);
}
// --- screen dimensions ---
// EmulationSetDeviceMetricsOverride sets the CSS viewport but leaves
// window.screen.* at headless defaults.
const screenProto = typeof Screen !== 'undefined' ? Screen.prototype : null;
if (screenProto) {
sealGetter(screenProto, 'width', () => __screenW);
sealGetter(screenProto, 'height', () => __screenH);
sealGetter(screenProto, 'availWidth', () => __availW);
sealGetter(screenProto, 'availHeight', () => __availH);
sealGetter(screenProto, 'availLeft', () => 0);
sealGetter(screenProto, 'availTop', () => __availTop);
// Headless reports colorDepth/pixelDepth=24 already in most builds, but
// some checks see 0 in WSL/Docker. Lock to 24 which matches real Chrome.
sealGetter(screenProto, 'colorDepth', () => 24);
sealGetter(screenProto, 'pixelDepth', () => 24);
}
if (typeof window !== 'undefined') {
sealGetter(window, 'outerWidth', () => __outerW);
sealGetter(window, 'outerHeight', () => __outerH);
}
// --- WebGL vendor/renderer spoof ---
// Patches getParameter(37445=UNMASKED_VENDOR_WEBGL, 37446=UNMASKED_RENDERER_WEBGL)
// on WebGLRenderingContext.prototype and WebGL2RenderingContext.prototype.
// Only delegates to the original for all other params, so behavioral
// signals (extension list, real pixel rendering) still pass through.
const patchWebGLProto = (proto) => {
if (!proto) return;
const original = proto.getParameter;
if (typeof original !== 'function') return;
const replacement = function getParameter(parameter) {
if (parameter === 37445) return __webglVendor;
if (parameter === 37446) return __webglRenderer;
return original.apply(this, arguments);
};
try {
Object.defineProperty(proto, 'getParameter', {
value: replacement,
writable: true,
enumerable: false,
configurable: true,
});
} catch (_) {}
};
if (typeof WebGLRenderingContext !== 'undefined') {
patchWebGLProto(WebGLRenderingContext.prototype);
}
if (typeof WebGL2RenderingContext !== 'undefined') {
patchWebGLProto(WebGL2RenderingContext.prototype);
}
// --- navigator.plugins / mimeTypes ---
// Match real Chrome 136 exactly: 5 plugins (all "internal-pdf-viewer"), each
// exposing application/pdf + text/pdf. navigator.mimeTypes dedupes to 2.
// Each MimeType.enabledPlugin must back-reference the FIRST plugin that owns
// that type (Chrome's invariant). Plugin order is fixed.
try {
const PluginArrayProto = typeof PluginArray !== 'undefined' ? PluginArray.prototype : null;
const PluginProto = typeof Plugin !== 'undefined' ? Plugin.prototype : null;
const MimeTypeArrayProto = typeof MimeTypeArray !== 'undefined' ? MimeTypeArray.prototype : null;
const MimeTypeProto = typeof MimeType !== 'undefined' ? MimeType.prototype : null;
if (PluginArrayProto && PluginProto && MimeTypeArrayProto && MimeTypeProto) {
const pluginNames = [
'PDF Viewer',
'Chrome PDF Viewer',
'Chromium PDF Viewer',
'Microsoft Edge PDF Viewer',
'WebKit built-in PDF',
];
const mimeSpecs = [
{ type: 'application/pdf', suffixes: 'pdf', description: 'Portable Document Format' },
{ type: 'text/pdf', suffixes: 'pdf', description: 'Portable Document Format' },
];
// Two MimeType instances, each enabledPlugin points to the first plugin
// (PDF Viewer) per Chrome's invariant: navigator.mimeTypes[i].enabledPlugin
// === navigator.plugins[0] for every PDF mime.
const sharedMimes = mimeSpecs.map((spec) => {
const m = Object.create(MimeTypeProto);
Object.defineProperty(m, 'type', { value: spec.type, enumerable: true });
Object.defineProperty(m, 'suffixes', { value: spec.suffixes, enumerable: true });
Object.defineProperty(m, 'description', { value: spec.description, enumerable: true });
return m;
});
const plugins = pluginNames.map((name) => {
const p = Object.create(PluginProto);
Object.defineProperty(p, 'name', { value: name, enumerable: true });
Object.defineProperty(p, 'filename', { value: 'internal-pdf-viewer', enumerable: true });
Object.defineProperty(p, 'description', { value: 'Portable Document Format', enumerable: true });
Object.defineProperty(p, 'length', { value: sharedMimes.length, enumerable: true });
sharedMimes.forEach((m, i) => {
Object.defineProperty(p, String(i), { value: m, enumerable: true });
Object.defineProperty(p, m.type, { value: m });
});
return p;
});
// Set enabledPlugin AFTER plugins are constructed, pointing to plugins[0].
sharedMimes.forEach((m) => {
Object.defineProperty(m, 'enabledPlugin', { value: plugins[0], enumerable: true });
});
const pluginArr = Object.create(PluginArrayProto);
Object.defineProperty(pluginArr, 'length', { value: plugins.length, enumerable: true });
plugins.forEach((p, i) => {
Object.defineProperty(pluginArr, String(i), { value: p, enumerable: true });
Object.defineProperty(pluginArr, p.name, { value: p });
});
const mimeArr = Object.create(MimeTypeArrayProto);
Object.defineProperty(mimeArr, 'length', { value: sharedMimes.length, enumerable: true });
sharedMimes.forEach((m, i) => {
Object.defineProperty(mimeArr, String(i), { value: m, enumerable: true });
Object.defineProperty(mimeArr, m.type, { value: m });
});
sealGetter(Object.getPrototypeOf(navigator), 'plugins', () => pluginArr);
sealGetter(Object.getPrototypeOf(navigator), 'mimeTypes', () => mimeArr);
}
} catch (_) {}
// --- navigator.permissions.query notifications fix ---
// headless returns 'denied' for notifications when Notification.permission is
// 'default'. Real Chrome returns 'prompt' in that case. Sannysoft checks this
// mismatch (permissions_new / headchr_permissions).
try {
if (navigator.permissions && typeof navigator.permissions.query === 'function') {
const proto = Object.getPrototypeOf(navigator.permissions);
const desc = Object.getOwnPropertyDescriptor(proto, 'query');
if (desc && typeof desc.value === 'function') {
const original = desc.value;
const replacement = function query(parameters) {
if (parameters && parameters.name === 'notifications' &&
typeof Notification !== 'undefined' && Notification.permission === 'default') {
return Promise.resolve({ state: 'prompt', onchange: null });
}
return original.apply(this, arguments);
};
Object.defineProperty(proto, 'query', {
value: replacement,
writable: desc.writable,
enumerable: desc.enumerable,
configurable: desc.configurable,
});
}
}
} catch (_) {}
// --- getBoundingClientRect / getClientRects subpixel jitter ---
// Headless Chrome returns integer-valued rects; real Chrome returns subpixel
// floats due to CSS layout fractions. Fingerprinters hash rect tuples; even
// a sub-pixel offset breaks the canonical "headless rect" hash.
// Jitter is deterministic per-element (based on element identity) so the
// same element returns the same value across calls within the page lifetime.
try {
const rectProto = typeof DOMRect !== 'undefined' ? DOMRect.prototype : null;
const elProto = typeof Element !== 'undefined' ? Element.prototype : null;
if (rectProto && elProto) {
const wmJitter = new WeakMap();
const jitterFor = (el) => {
let j = wmJitter.get(el);
if (!j) {
// Tiny noise in [-0.05, +0.05) — well below visual threshold but
// changes hash output. Generated once per element.
j = {
x: (Math.random() - 0.5) * 0.1,
y: (Math.random() - 0.5) * 0.1,
};
wmJitter.set(el, j);
}
return j;
};
const origGBCR = elProto.getBoundingClientRect;
Object.defineProperty(elProto, 'getBoundingClientRect', {
value: function getBoundingClientRect() {
const r = origGBCR.apply(this, arguments);
const j = jitterFor(this);
// DOMRect is mutable; nudge x/y. width/height left intact so layout
// calculations don't drift.
try { r.x = r.x + j.x; r.y = r.y + j.y; } catch (_) {}
return r;
},
writable: true,
enumerable: false,
configurable: true,
});
}
} catch (_) {}
// --- window.chrome.runtime stub ---
// Real Chrome exposes window.chrome with a .runtime sub-object.
// headless leaves window.chrome empty, which sannysoft (chrome_new,
// headchr_chrome_obj) flags. A minimal runtime stub satisfies the check
// without touching method behavior.
try {
if (typeof window !== 'undefined') {
if (!window.chrome) {
Object.defineProperty(window, 'chrome', { value: {}, writable: true, configurable: true });
}
if (window.chrome && !window.chrome.runtime) {
Object.defineProperty(window.chrome, 'runtime', {
value: {
OnInstalledReason: { CHROME_UPDATE: 'chrome_update', INSTALL: 'install', UPDATE: 'update' },
OnRestartRequiredReason: { APP_UPDATE: 'app_update', OS_UPDATE: 'os_update', PERIODIC: 'periodic' },
PlatformOs: { ANDROID: 'android', CROS: 'cros', LINUX: 'linux', MAC: 'mac', WIN: 'win' },
},
writable: true,
enumerable: true,
configurable: true,
});
}
}
} catch (_) {}
})();