From 95fe4af6ec73a6ea41b390680079d295b2005aca Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Fri, 14 Aug 2026 18:46:59 -0700 Subject: [PATCH] fix(browser): align anti-detection identity signals --- src/main/browser/anti-detection.test.ts | 55 ++++++++++++++++++ src/main/browser/anti-detection.ts | 74 ++++++++++++------------- 2 files changed, 91 insertions(+), 38 deletions(-) diff --git a/src/main/browser/anti-detection.test.ts b/src/main/browser/anti-detection.test.ts index c937e3342c6..b191e612024 100644 --- a/src/main/browser/anti-detection.test.ts +++ b/src/main/browser/anti-detection.test.ts @@ -2,6 +2,7 @@ import { runInNewContext } from 'node:vm' import { describe, expect, it } from 'vitest' import { ANTI_DETECTION_SCRIPT } from './anti-detection' +import { googleAuthUserAgent } from './browser-google-auth-ua' type PermissionQueryResult = { state: string @@ -14,15 +15,24 @@ type AntiDetectionContext = { requestPermission: (callback?: (permission: string) => void) => Promise } navigator: { + userAgent: string permissions: { query: (descriptor: { name: string }) => Promise } } + window: { + chrome?: { + runtime?: unknown + csi?: () => unknown + loadTimes?: () => unknown + } + } } function createContext(args: { nativeNotificationPermission: string requestedNotificationPermission: string + userAgent?: string }): AntiDetectionContext & Record { class Permissions { query(): Promise { @@ -50,6 +60,9 @@ function createContext(args: { performance: { now: () => 0 }, window: {}, navigator: { + userAgent: + args.userAgent ?? + 'Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36', plugins: [], languages: [], permissions: new Permissions() @@ -60,6 +73,48 @@ function createContext(args: { } describe('ANTI_DETECTION_SCRIPT', () => { + it('does not expose Chrome globals under a Firefox identity', () => { + const context = createContext({ + nativeNotificationPermission: 'denied', + requestedNotificationPermission: 'denied', + userAgent: googleAuthUserAgent() + }) + + runInNewContext(ANTI_DETECTION_SCRIPT, context) + + expect(context.window.chrome).toBeUndefined() + }) + + it('keeps Chrome API stubs aligned with an ordinary Chrome page', () => { + const context = createContext({ + nativeNotificationPermission: 'denied', + requestedNotificationPermission: 'denied' + }) + + runInNewContext(ANTI_DETECTION_SCRIPT, context) + + expect(context.window.chrome?.runtime).toBeUndefined() + expect(context.window.chrome?.csi).toBeTypeOf('function') + expect(context.window.chrome?.loadTimes).toBeTypeOf('function') + }) + + it.each(['geolocation', 'idle-detection', 'midi', 'storage-access'])( + 'preserves the native denied state for %s', + async (name) => { + const context = createContext({ + nativeNotificationPermission: 'denied', + requestedNotificationPermission: 'denied' + }) + + runInNewContext(ANTI_DETECTION_SCRIPT, context) + + await expect(context.navigator.permissions.query({ name })).resolves.toEqual({ + state: 'denied', + onchange: null + }) + } + ) + it('reports notification permission as granted after a site permission request succeeds', async () => { const context = createContext({ nativeNotificationPermission: 'denied', diff --git a/src/main/browser/anti-detection.ts b/src/main/browser/anti-detection.ts index 4c62850ddc6..9b7cb991edb 100644 --- a/src/main/browser/anti-detection.ts +++ b/src/main/browser/anti-detection.ts @@ -16,44 +16,43 @@ export const ANTI_DETECTION_SCRIPT = `(function() { ] }); } - // Why: Electron webviews may not have the window.chrome object that real - // Chrome exposes. Turnstile checks for its presence. The csi() and - // loadTimes() stubs satisfy deeper probes that check for these Chrome- - // specific APIs beyond just chrome.runtime. - if (!window.chrome) { - window.chrome = {}; - } - if (!window.chrome.runtime) { - window.chrome.runtime = {}; - } - if (!window.chrome.csi) { - window.chrome.csi = function() { - return { - startE: Date.now(), - onloadT: Date.now(), - pageT: performance.now(), - tran: 15 + // Why: auth hosts present Firefox, where exposing Chrome-only globals is a direct identity mismatch. + if (!navigator.userAgent.includes('Firefox/')) { + // Why: Electron webviews may not have the window.chrome object that real + // Chrome exposes. Turnstile checks for its presence. The csi() and + // loadTimes() stubs satisfy deeper probes of Chrome-specific APIs. + if (!window.chrome) { + window.chrome = {}; + } + if (!window.chrome.csi) { + window.chrome.csi = function() { + return { + startE: Date.now(), + onloadT: Date.now(), + pageT: performance.now(), + tran: 15 + }; }; - }; - } - if (!window.chrome.loadTimes) { - window.chrome.loadTimes = function() { - return { - commitLoadTime: Date.now() / 1000, - connectionInfo: 'h2', - finishDocumentLoadTime: Date.now() / 1000, - finishLoadTime: Date.now() / 1000, - firstPaintAfterLoadTime: 0, - firstPaintTime: Date.now() / 1000, - navigationType: 'Other', - npnNegotiatedProtocol: 'h2', - requestTime: Date.now() / 1000 - 0.16, - startLoadTime: Date.now() / 1000 - 0.3, - wasAlternateProtocolAvailable: false, - wasFetchedViaSpdy: true, - wasNpnNegotiated: true + } + if (!window.chrome.loadTimes) { + window.chrome.loadTimes = function() { + return { + commitLoadTime: Date.now() / 1000, + connectionInfo: 'h2', + finishDocumentLoadTime: Date.now() / 1000, + finishLoadTime: Date.now() / 1000, + firstPaintAfterLoadTime: 0, + firstPaintTime: Date.now() / 1000, + navigationType: 'Other', + npnNegotiatedProtocol: 'h2', + requestTime: Date.now() / 1000 - 0.16, + startLoadTime: Date.now() / 1000 - 0.3, + wasAlternateProtocolAvailable: false, + wasFetchedViaSpdy: true, + wasNpnNegotiated: true + }; }; - }; + } } // Why: Electron's Permission API defaults to 'denied' for most permissions, // but real Chrome returns 'prompt' for ungranted permissions. Returning @@ -77,8 +76,7 @@ export const ANTI_DETECTION_SCRIPT = `(function() { } } catch {} const promptPerms = new Set([ - 'geolocation', 'camera', 'microphone', - 'midi', 'idle-detection', 'storage-access' + 'camera', 'microphone' ]); const origQuery = Permissions.prototype.query; Permissions.prototype.query = function(desc) {