From 067c1989e2ad304ed4988e47bc9113354fdac4ec Mon Sep 17 00:00:00 2001 From: LightZirconite Date: Sat, 6 Dec 2025 14:54:02 +0100 Subject: [PATCH] feat: implement fingerprint validation and age check to enhance anti-detection measures --- src/browser/Browser.ts | 179 ++++++++++++++++- src/util/state/Load.ts | 11 + src/util/validation/FingerprintValidator.ts | 212 ++++++++++++++++++++ 3 files changed, 397 insertions(+), 5 deletions(-) create mode 100644 src/util/validation/FingerprintValidator.ts diff --git a/src/browser/Browser.ts b/src/browser/Browser.ts index 0c3b520..a00c931 100644 --- a/src/browser/Browser.ts +++ b/src/browser/Browser.ts @@ -6,6 +6,7 @@ import { MicrosoftRewardsBot } from '../index' import { AccountProxy } from '../interface/Account' import { updateFingerprintUserAgent } from '../util/browser/UserAgent' import { loadSessionData, saveFingerprintData } from '../util/state/Load' +import { logFingerprintValidation, validateFingerprintConsistency } from '../util/validation/FingerprintValidator' class Browser { private bot: MicrosoftRewardsBot @@ -40,14 +41,33 @@ class Browser { const isLinux = process.platform === 'linux' - // Base arguments for stability + // CRITICAL: Anti-detection Chromium arguments const baseArgs = [ '--no-sandbox', '--mute-audio', '--disable-setuid-sandbox', '--ignore-certificate-errors', '--ignore-certificate-errors-spki-list', - '--ignore-ssl-errors' + '--ignore-ssl-errors', + // ANTI-DETECTION: Disable blink features that expose automation + '--disable-blink-features=AutomationControlled', + // ANTI-DETECTION: Disable automation extensions + '--disable-extensions', + // ANTI-DETECTION: Start maximized (humans rarely start in specific window sizes) + '--start-maximized', + // ANTI-DETECTION: Disable save password bubble + '--disable-save-password-bubble', + // ANTI-DETECTION: Disable background timer throttling + '--disable-background-timer-throttling', + '--disable-backgrounding-occluded-windows', + '--disable-renderer-backgrounding', + // ANTI-DETECTION: Disable infobars + '--disable-infobars', + // PERFORMANCE: Disable unnecessary features + '--disable-breakpad', + '--disable-component-update', + '--no-first-run', + '--no-default-browser-check' ] // Linux stability fixes @@ -80,6 +100,16 @@ class Browser { const sessionData = await loadSessionData(this.bot.config.sessionPath, email, this.bot.isMobile, saveFingerprint) const fingerprint = sessionData.fingerprint ? sessionData.fingerprint : await this.generateFingerprint() + + // CRITICAL: Validate fingerprint consistency before using it + const validationResult = validateFingerprintConsistency(fingerprint, this.bot.config) + logFingerprintValidation(validationResult, email) + + // SECURITY: Abort if critical issues detected (optional, can be disabled) + if (!validationResult.valid && this.bot.config.riskManagement?.stopOnCritical) { + throw new Error(`Fingerprint validation failed for ${email}: ${validationResult.criticalIssues.join(', ')}`) + } + const context = await newInjectedContext(browser as unknown as import('playwright').Browser, { fingerprint: fingerprint }) const globalTimeout = this.bot.config.browser?.globalTimeout ?? 30000 @@ -88,14 +118,153 @@ class Browser { try { context.on('page', async (page) => { try { + // IMPROVED: Randomized viewport sizes to avoid fingerprinting + // Fixed sizes are detectable bot patterns const viewport = this.bot.isMobile - ? { width: 390, height: 844 } - : { width: 1280, height: 800 } + ? { + // Mobile: Vary between common phone screen sizes + width: 360 + Math.floor(Math.random() * 60), // 360-420px + height: 640 + Math.floor(Math.random() * 256) // 640-896px + } + : { + // Desktop: Vary between common desktop resolutions + width: 1280 + Math.floor(Math.random() * 640), // 1280-1920px + height: 720 + Math.floor(Math.random() * 360) // 720-1080px + } await page.setViewportSize(viewport) - // Standard styling + // CRITICAL: Advanced anti-detection scripts (MUST run before page load) await page.addInitScript(() => { + // ═══════════════════════════════════════════════════════════════ + // ANTI-DETECTION LAYER 1: Remove automation indicators + // ═══════════════════════════════════════════════════════════════ + + // CRITICAL: Remove navigator.webdriver (biggest bot indicator) + try { + Object.defineProperty(navigator, 'webdriver', { + get: () => undefined, + configurable: true + }) + } catch { /* Already defined */ } + + // CRITICAL: Mask Chrome DevTools Protocol detection + // Microsoft checks for window.chrome.runtime + try { + // @ts-ignore - window.chrome is intentionally injected + if (!window.chrome) { + // @ts-ignore + window.chrome = {} + } + // @ts-ignore + if (!window.chrome.runtime) { + // @ts-ignore + window.chrome.runtime = { + // @ts-ignore + connect: () => { }, + // @ts-ignore + sendMessage: () => { } + } + } + } catch { /* Chrome object may be frozen */ } + + // ═══════════════════════════════════════════════════════════════ + // ANTI-DETECTION LAYER 2: WebGL & Canvas fingerprint randomization + // ═══════════════════════════════════════════════════════════════ + + // CRITICAL: Add noise to Canvas fingerprinting + // Microsoft uses Canvas to detect identical browser instances + try { + const originalToDataURL = HTMLCanvasElement.prototype.toDataURL + const originalGetImageData = CanvasRenderingContext2D.prototype.getImageData + + // Random noise generator (consistent per page load, different per session) + const noise = Math.random() * 0.0001 + + HTMLCanvasElement.prototype.toDataURL = function (...args) { + const context = this.getContext('2d') + if (context) { + // Add imperceptible noise + const imageData = context.getImageData(0, 0, this.width, this.height) + for (let i = 0; i < imageData.data.length; i += 4) { + imageData.data[i] = imageData.data[i]! + noise // R + imageData.data[i + 1] = imageData.data[i + 1]! + noise // G + imageData.data[i + 2] = imageData.data[i + 2]! + noise // B + } + context.putImageData(imageData, 0, 0) + } + return originalToDataURL.apply(this, args) + } + + CanvasRenderingContext2D.prototype.getImageData = function (...args) { + const imageData = originalGetImageData.apply(this, args) + // Add noise to raw pixel data + for (let i = 0; i < imageData.data.length; i += 10) { + imageData.data[i] = imageData.data[i]! + noise + } + return imageData + } + } catch { /* Canvas override may fail in strict mode */ } + + // CRITICAL: WebGL fingerprint randomization + try { + const getParameter = WebGLRenderingContext.prototype.getParameter + WebGLRenderingContext.prototype.getParameter = function (parameter) { + // Randomize UNMASKED_VENDOR_WEBGL and UNMASKED_RENDERER_WEBGL + if (parameter === 37445) { // UNMASKED_VENDOR_WEBGL + return 'Intel Inc.' + } + if (parameter === 37446) { // UNMASKED_RENDERER_WEBGL + return 'Intel Iris OpenGL Engine' + } + return getParameter.apply(this, [parameter]) + } + } catch { /* WebGL override may fail */ } + + // ═══════════════════════════════════════════════════════════════ + // ANTI-DETECTION LAYER 3: Permissions API masking + // ═══════════════════════════════════════════════════════════════ + + // CRITICAL: Mask permissions query (bots have different permissions) + try { + const originalQuery = navigator.permissions.query + // @ts-ignore + navigator.permissions.query = (parameters) => { + // Always return 'prompt' for notifications (human-like) + if (parameters.name === 'notifications') { + return Promise.resolve({ state: 'prompt', onchange: null }) + } + return originalQuery(parameters) + } + } catch { /* Permissions API may not be available */ } + + // ═══════════════════════════════════════════════════════════════ + // ANTI-DETECTION LAYER 4: Plugin/MIME type consistency + // ═══════════════════════════════════════════════════════════════ + + // CRITICAL: Add realistic plugins (headless browsers have none) + try { + Object.defineProperty(navigator, 'plugins', { + get: () => [ + { + name: 'PDF Viewer', + description: 'Portable Document Format', + filename: 'internal-pdf-viewer', + length: 2 + }, + { + name: 'Chrome PDF Viewer', + description: 'Portable Document Format', + filename: 'internal-pdf-viewer', + length: 2 + } + ] + }) + } catch { /* Plugins may be frozen */ } + + // ═══════════════════════════════════════════════════════════════ + // Standard styling (non-detection related) + // ═══════════════════════════════════════════════════════════════ try { const style = document.createElement('style') style.id = '__mrs_fit_style' diff --git a/src/util/state/Load.ts b/src/util/state/Load.ts index 341234f..bfb6bdc 100644 --- a/src/util/state/Load.ts +++ b/src/util/state/Load.ts @@ -472,6 +472,17 @@ export async function loadSessionData(sessionPath: string, email: string, isMobi if (shouldLoad && fs.existsSync(fingerprintFile)) { const fingerprintData = await fs.promises.readFile(fingerprintFile, 'utf-8') fingerprint = JSON.parse(fingerprintData) + + // CRITICAL: Validate fingerprint age (regenerate if too old) + // Old fingerprints become suspicious as browser versions update + const fingerprintStat = await fs.promises.stat(fingerprintFile) + const ageInDays = (Date.now() - fingerprintStat.mtimeMs) / (1000 * 60 * 60 * 24) + + // SECURITY: Regenerate fingerprint if older than 30 days + if (ageInDays > 30) { + // Mark as undefined to trigger regeneration + fingerprint = undefined as any + } } return { diff --git a/src/util/validation/FingerprintValidator.ts b/src/util/validation/FingerprintValidator.ts new file mode 100644 index 0000000..bb043a9 --- /dev/null +++ b/src/util/validation/FingerprintValidator.ts @@ -0,0 +1,212 @@ +/** + * Fingerprint Consistency Validator + * + * CRITICAL: Microsoft detects automation by checking for inconsistencies between: + * - Timezone (browser reported vs. IP geolocation) + * - Locale (browser language vs. IP country) + * - Screen resolution (realistic vs. bot patterns) + * - WebGL renderer (consistency check) + * + * This validator warns about potential detection risks BEFORE running automation. + */ + +import type { BrowserFingerprintWithHeaders } from 'fingerprint-generator' +import type { Config } from '../../interface/Config' +import { log } from '../notifications/Logger' + +export interface FingerprintValidationResult { + valid: boolean + warnings: string[] + criticalIssues: string[] +} + +/** + * Validate fingerprint consistency to minimize detection risk + * @param fingerprint Browser fingerprint data + * @param config Bot configuration + * @returns Validation result with warnings/errors + */ +export function validateFingerprintConsistency( + fingerprint: BrowserFingerprintWithHeaders, + config: Config +): FingerprintValidationResult { + const warnings: string[] = [] + const criticalIssues: string[] = [] + + // ═══════════════════════════════════════════════════════════════════════ + // VALIDATION 1: Timezone consistency + // ═══════════════════════════════════════════════════════════════════════ + + try { + const browserTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone + const fingerprintTimezone = fingerprint.fingerprint.navigator?.userAgentData?.platform + + // CRITICAL: Check if timezone makes sense for the platform + if (fingerprintTimezone === 'Windows') { + // Windows users rarely use non-UTC timezones + if (browserTimezone && !browserTimezone.includes('UTC') && !browserTimezone.includes('America') && !browserTimezone.includes('Europe')) { + warnings.push(`Timezone '${browserTimezone}' is unusual for Windows platform (${fingerprintTimezone})`) + } + } + + if (fingerprintTimezone === 'Android') { + // Mobile users should have timezone matching their location + // If using proxy, timezone may not match IP location (detection risk) + warnings.push('Mobile timezone consistency cannot be validated without IP geolocation data') + } + } catch { + // Timezone validation failed (non-critical) + } + + // ═══════════════════════════════════════════════════════════════════════ + // VALIDATION 2: Screen resolution realism + // ═══════════════════════════════════════════════════════════════════════ + + try { + const screen = fingerprint.fingerprint.screen + if (screen) { + const { width, height, availWidth, availHeight } = screen + + // CRITICAL: Check for unrealistic screen dimensions + if (width < 800 && height < 600) { + criticalIssues.push(`Screen size too small: ${width}x${height} (likely bot pattern)`) + } + + if (width > 7680 || height > 4320) { + warnings.push(`Screen size unusually large: ${width}x${height} (8K+)`) + } + + // CRITICAL: Check for exact match between screen and available size (bot pattern) + if (width === availWidth && height === availHeight) { + warnings.push('Screen size exactly matches available size (possible bot pattern - no taskbar/menubar)') + } + + // CRITICAL: Check devicePixelRatio realism + const dpr = screen.devicePixelRatio + if (dpr && (dpr < 0.5 || dpr > 5)) { + warnings.push(`Device pixel ratio unusual: ${dpr} (expected 0.5-5)`) + } + } + } catch { + // Screen validation failed (non-critical) + } + + // ═══════════════════════════════════════════════════════════════════════ + // VALIDATION 3: User agent consistency + // ═══════════════════════════════════════════════════════════════════════ + + try { + const ua = fingerprint.fingerprint.navigator.userAgent + const uaPlatform = fingerprint.fingerprint.navigator?.userAgentData?.platform + + // CRITICAL: Check for mismatched platform indicators + if (ua.includes('Windows') && uaPlatform !== 'Windows') { + criticalIssues.push(`User agent platform mismatch: UA says Windows, platform says ${uaPlatform}`) + } + + if (ua.includes('Android') && uaPlatform !== 'Android') { + criticalIssues.push(`User agent platform mismatch: UA says Android, platform says ${uaPlatform}`) + } + + // CRITICAL: Check for outdated browser versions (bot indicator) + const chromeMatch = ua.match(/Chrome\/(\d+)/) + if (chromeMatch) { + const chromeVersion = parseInt(chromeMatch[1] || '0') + const currentYear = new Date().getFullYear() + const currentMonth = new Date().getMonth() + 1 + + // Chrome releases ~6 versions per year (every 2 months) + // Rough estimation: version 100 in 2022, +6 per year + const expectedMinVersion = 100 + ((currentYear - 2022) * 6) + Math.floor(currentMonth / 2) + + if (chromeVersion < expectedMinVersion - 20) { + warnings.push(`Chrome version ${chromeVersion} is outdated (expected ${expectedMinVersion - 20}+)`) + } + } + } catch { + // UA validation failed (non-critical) + } + + // ═══════════════════════════════════════════════════════════════════════ + // VALIDATION 4: Header consistency + // ═══════════════════════════════════════════════════════════════════════ + + try { + const headers = fingerprint.headers + + // CRITICAL: Check for missing critical headers (bot indicator) + const requiredHeaders = ['user-agent', 'accept', 'accept-language', 'sec-ch-ua'] + for (const header of requiredHeaders) { + if (!headers[header]) { + criticalIssues.push(`Missing critical header: ${header}`) + } + } + + // CRITICAL: Check sec-ch-ua consistency with user agent + if (headers['sec-ch-ua'] && headers['user-agent']) { + const secChUa = headers['sec-ch-ua'] + const ua = headers['user-agent'] + + // Extract Edge version from sec-ch-ua + const edgeMatch = secChUa.match(/"Microsoft Edge";v="(\d+)"/) + const uaEdgeMatch = ua.match(/Edg[A]?\/(\d+)/) + + if (edgeMatch && uaEdgeMatch) { + const secChVersion = edgeMatch[1] + const uaVersion = uaEdgeMatch[1] + + if (secChVersion !== uaVersion) { + criticalIssues.push(`Edge version mismatch: sec-ch-ua=${secChVersion}, user-agent=${uaVersion}`) + } + } + } + } catch { + // Header validation failed (non-critical) + } + + // ═══════════════════════════════════════════════════════════════════════ + // VALIDATION 5: Fingerprint persistence check + // ═══════════════════════════════════════════════════════════════════════ + + if (!config.saveFingerprint?.desktop && !config.saveFingerprint?.mobile) { + warnings.push('Fingerprint persistence disabled - each run generates new fingerprint (high detection risk)') + } + + // ═══════════════════════════════════════════════════════════════════════ + // Final verdict + // ═══════════════════════════════════════════════════════════════════════ + + const valid = criticalIssues.length === 0 + + return { + valid, + warnings, + criticalIssues + } +} + +/** + * Log fingerprint validation results + * @param result Validation result + * @param email Account email for context + */ +export function logFingerprintValidation(result: FingerprintValidationResult, email: string): void { + if (result.criticalIssues.length > 0) { + log('main', 'FINGERPRINT', `⚠️ CRITICAL ISSUES detected for ${email}:`, 'error') + result.criticalIssues.forEach(issue => { + log('main', 'FINGERPRINT', ` ❌ ${issue}`, 'error') + }) + log('main', 'FINGERPRINT', '🚨 Account may be flagged as bot - high ban risk!', 'error') + } + + if (result.warnings.length > 0 && result.criticalIssues.length === 0) { + log('main', 'FINGERPRINT', `⚠️ Warnings for ${email}:`, 'warn') + result.warnings.forEach(warning => { + log('main', 'FINGERPRINT', ` ⚠️ ${warning}`, 'warn') + }) + } + + if (result.valid && result.warnings.length === 0) { + log('main', 'FINGERPRINT', `✅ Fingerprint validation passed for ${email}`, 'log') + } +}