mirror of
https://github.com/stablyai/orca.git
synced 2026-10-22 16:03:47 +00:00
fix(browser): align anti-detection identity signals
This commit is contained in:
@@ -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<string>
|
||||
}
|
||||
navigator: {
|
||||
userAgent: string
|
||||
permissions: {
|
||||
query: (descriptor: { name: string }) => Promise<PermissionQueryResult>
|
||||
}
|
||||
}
|
||||
window: {
|
||||
chrome?: {
|
||||
runtime?: unknown
|
||||
csi?: () => unknown
|
||||
loadTimes?: () => unknown
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createContext(args: {
|
||||
nativeNotificationPermission: string
|
||||
requestedNotificationPermission: string
|
||||
userAgent?: string
|
||||
}): AntiDetectionContext & Record<string, unknown> {
|
||||
class Permissions {
|
||||
query(): Promise<PermissionQueryResult> {
|
||||
@@ -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',
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user