From 4772b153b8f7024c8cf17b7929a1bff96537a90f Mon Sep 17 00:00:00 2001 From: Steve Dudenhoeffer Date: Tue, 24 Feb 2026 01:38:14 +0000 Subject: [PATCH] fix: randomize hardware fingerprint values across browser sessions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace static stealthChromiumScripts and stealthFirefoxScripts slices with builder functions that accept hardware profile structs. Each browser session now randomly selects from a pool of 6 realistic profiles per engine, and Chromium connection stats receive per-session jitter (±20ms RTT, ±2 Mbps downlink). This prevents anti-bot systems from correlating sessions via identical WebGL, connection, mozInnerScreen, and hardwareConcurrency fingerprints. Closes #71 Co-Authored-By: Claude Opus 4.6 --- browser_init.go | 4 +- stealth.go | 165 ++++++++++++++++++++++++++++++++++-------------- stealth_test.go | 134 +++++++++++++++++++++++++++++++++------ 3 files changed, 233 insertions(+), 70 deletions(-) diff --git a/browser_init.go b/browser_init.go index 5236adf..5ceeec7 100644 --- a/browser_init.go +++ b/browser_init.go @@ -74,9 +74,9 @@ func initBrowser(opt BrowserOptions) (*browserInitResult, error) { initScripts = append(initScripts, stealthCommonScripts...) switch opt.Browser { case BrowserChromium: - initScripts = append(initScripts, stealthChromiumScripts...) + initScripts = append(initScripts, buildChromiumStealthScripts(randomChromiumProfile())...) case BrowserFirefox: - initScripts = append(initScripts, stealthFirefoxScripts...) + initScripts = append(initScripts, buildFirefoxStealthScripts(randomFirefoxProfile())...) } } diff --git a/stealth.go b/stealth.go index adc2b74..c61c63e 100644 --- a/stealth.go +++ b/stealth.go @@ -1,5 +1,10 @@ package extractor +import ( + "fmt" + "math/rand/v2" +) + // stealthChromiumArgs are launch arguments that reduce automation detection for Chromium-based browsers. var stealthChromiumArgs = []string{ "--disable-blink-features=AutomationControlled", @@ -39,10 +44,47 @@ var stealthCommonScripts = []string{ })()`, } -// stealthChromiumScripts are JavaScript snippets specific to Chromium-based browsers. -var stealthChromiumScripts = []string{ - // Populate navigator.plugins with realistic Chromium entries so plugins.length > 0. - `Object.defineProperty(navigator, 'plugins', { +// chromiumHWProfile holds hardware fingerprint values for a Chromium browser session. +type chromiumHWProfile struct { + WebGLVendor string + WebGLRenderer string + ConnRTT int // base RTT in ms (jittered ±20 per session) + ConnDownlink float64 // base downlink in Mbps (jittered ±2 per session) +} + +// chromiumHWProfiles is a pool of realistic Chromium hardware profiles. +// Index 0 matches the original hardcoded values. +var chromiumHWProfiles = []chromiumHWProfile{ + {"Google Inc. (Intel)", "ANGLE (Intel, Intel(R) UHD Graphics 630, OpenGL 4.5)", 50, 10}, + {"Google Inc. (NVIDIA)", "ANGLE (NVIDIA, NVIDIA GeForce GTX 1660 SUPER, D3D11)", 30, 25}, + {"Google Inc. (AMD)", "ANGLE (AMD, AMD Radeon RX 580, D3D11)", 100, 5}, + {"Google Inc. (Intel)", "ANGLE (Intel, Intel(R) UHD Graphics 770, OpenGL 4.5)", 50, 10}, + {"Google Inc. (NVIDIA)", "ANGLE (NVIDIA, NVIDIA GeForce RTX 3060, D3D11)", 25, 50}, + {"Google Inc. (Intel)", "ANGLE (Intel, Intel(R) Iris Xe Graphics, D3D11)", 75, 8}, +} + +// randomChromiumProfile returns a randomly selected Chromium hardware profile. +func randomChromiumProfile() chromiumHWProfile { + return chromiumHWProfiles[rand.IntN(len(chromiumHWProfiles))] +} + +// buildChromiumStealthScripts returns Chromium stealth init scripts with the given hardware profile +// values templated into the WebGL and connection spoofing scripts. Connection RTT and downlink +// receive per-session jitter (±20ms RTT, ±2 Mbps downlink). +func buildChromiumStealthScripts(p chromiumHWProfile) []string { + // Apply jitter to connection stats. + rtt := p.ConnRTT + rand.IntN(41) - 20 // ±20ms + if rtt < 0 { + rtt = 0 + } + downlink := p.ConnDownlink + (rand.Float64()*4 - 2) // ±2 Mbps + if downlink < 0.5 { + downlink = 0.5 + } + + return []string{ + // Populate navigator.plugins with realistic Chromium entries so plugins.length > 0. + `Object.defineProperty(navigator, 'plugins', { get: () => { const arr = [ { name: 'PDF Viewer', filename: 'internal-pdf-viewer', description: 'Portable Document Format' }, @@ -56,8 +98,8 @@ var stealthChromiumScripts = []string{ }, })`, - // Populate navigator.mimeTypes to match the fake Chromium plugins above. - `Object.defineProperty(navigator, 'mimeTypes', { + // Populate navigator.mimeTypes to match the fake Chromium plugins above. + `Object.defineProperty(navigator, 'mimeTypes', { get: () => { const arr = [ { type: 'application/pdf', suffixes: 'pdf', description: 'Portable Document Format' }, @@ -68,13 +110,13 @@ var stealthChromiumScripts = []string{ }, })`, - // Provide window.chrome runtime stub (Chromium-only signal). - `if (!window.chrome) { + // Provide window.chrome runtime stub (Chromium-only signal). + `if (!window.chrome) { window.chrome = { runtime: {} }; }`, - // Add chrome.app, chrome.csi, and chrome.loadTimes stubs missing in headless. - `(function() { + // Add chrome.app, chrome.csi, and chrome.loadTimes stubs missing in headless. + `(function() { if (!window.chrome) window.chrome = {}; if (!window.chrome.app) { window.chrome.app = { isInstalled: false, InstallState: { DISABLED: 'disabled', INSTALLED: 'installed', NOT_INSTALLED: 'not_installed' }, RunningState: { CANNOT_RUN: 'cannot_run', READY_TO_RUN: 'ready_to_run', RUNNING: 'running' } }; @@ -87,37 +129,37 @@ var stealthChromiumScripts = []string{ } })()`, - // Spoof WebGL renderer to hide SwiftShader (headless GPU) fingerprint with Chromium ANGLE strings. - `(function() { + // Spoof WebGL renderer to hide SwiftShader (headless GPU) fingerprint with Chromium ANGLE strings. + fmt.Sprintf(`(function() { const getParam = WebGLRenderingContext.prototype.getParameter; WebGLRenderingContext.prototype.getParameter = function(param) { - if (param === 37445) return 'Google Inc. (Intel)'; - if (param === 37446) return 'ANGLE (Intel, Intel(R) UHD Graphics 630, OpenGL 4.5)'; + if (param === 37445) return '%s'; + if (param === 37446) return '%s'; return getParam.call(this, param); }; if (typeof WebGL2RenderingContext !== 'undefined') { const getParam2 = WebGL2RenderingContext.prototype.getParameter; WebGL2RenderingContext.prototype.getParameter = function(param) { - if (param === 37445) return 'Google Inc. (Intel)'; - if (param === 37446) return 'ANGLE (Intel, Intel(R) UHD Graphics 630, OpenGL 4.5)'; + if (param === 37445) return '%s'; + if (param === 37446) return '%s'; return getParam2.call(this, param); }; } - })()`, + })()`, p.WebGLVendor, p.WebGLRenderer, p.WebGLVendor, p.WebGLRenderer), - // Stub navigator.connection (Network Information API) if missing (Chrome-only API). - `(function() { + // Stub navigator.connection (Network Information API) if missing (Chrome-only API). + fmt.Sprintf(`(function() { if (!navigator.connection) { Object.defineProperty(navigator, 'connection', { get: function() { - return { effectiveType: '4g', rtt: 50, downlink: 10, saveData: false, onchange: null }; + return { effectiveType: '4g', rtt: %d, downlink: %.1f, saveData: false, onchange: null }; }, }); } - })()`, + })()`, rtt, downlink), - // Remove CDP artifacts (window.cdc_* globals injected by Chrome DevTools Protocol). - `(function() { + // Remove CDP artifacts (window.cdc_* globals injected by Chrome DevTools Protocol). + `(function() { for (var key in window) { if (key.match(/^cdc_/)) { delete window[key]; @@ -125,8 +167,8 @@ var stealthChromiumScripts = []string{ } })()`, - // Strip "HeadlessChrome" from navigator.userAgent if present. - `(function() { + // Strip "HeadlessChrome" from navigator.userAgent if present. + `(function() { var ua = navigator.userAgent; if (ua.indexOf('HeadlessChrome') !== -1) { Object.defineProperty(navigator, 'userAgent', { @@ -134,12 +176,40 @@ var stealthChromiumScripts = []string{ }); } })()`, + } } -// stealthFirefoxScripts are JavaScript snippets specific to Firefox. -var stealthFirefoxScripts = []string{ - // Harden navigator.webdriver for Firefox: ensure Object.getOwnPropertyDescriptor also returns undefined. - `(function() { +// firefoxHWProfile holds hardware fingerprint values for a Firefox browser session. +type firefoxHWProfile struct { + WebGLVendor string + WebGLRenderer string + MozInnerScreenX int + MozInnerScreenY int + HardwareConcurrency int +} + +// firefoxHWProfiles is a pool of realistic Firefox hardware profiles. +// Index 0 matches the original hardcoded values. +var firefoxHWProfiles = []firefoxHWProfile{ + {"Intel Open Source Technology Center", "Mesa DRI Intel(R) UHD Graphics 630", 8, 51, 4}, + {"Intel Open Source Technology Center", "Mesa DRI Intel(R) HD Graphics 530", 0, 71, 8}, + {"X.Org", "AMD Radeon RX 580 (polaris10, LLVM 15.0.7, DRM 3.49, 6.1.0-18-amd64)", 8, 51, 8}, + {"Intel Open Source Technology Center", "Mesa DRI Intel(R) UHD Graphics 770", 0, 51, 16}, + {"nouveau", "NV167", 8, 71, 4}, + {"Intel", "Mesa Intel(R) Iris(R) Xe Graphics", 0, 51, 8}, +} + +// randomFirefoxProfile returns a randomly selected Firefox hardware profile. +func randomFirefoxProfile() firefoxHWProfile { + return firefoxHWProfiles[rand.IntN(len(firefoxHWProfiles))] +} + +// buildFirefoxStealthScripts returns Firefox stealth init scripts with the given hardware profile +// values templated into the WebGL, mozInnerScreen, and hardwareConcurrency spoofing scripts. +func buildFirefoxStealthScripts(p firefoxHWProfile) []string { + return []string{ + // Harden navigator.webdriver for Firefox: ensure Object.getOwnPropertyDescriptor also returns undefined. + `(function() { const proto = Object.getPrototypeOf(navigator); const origGetOwnPropDesc = Object.getOwnPropertyDescriptor; Object.getOwnPropertyDescriptor = function(obj, prop) { @@ -150,43 +220,43 @@ var stealthFirefoxScripts = []string{ }; })()`, - // Spoof WebGL renderer with Firefox-appropriate Mesa/Intel strings. - `(function() { + // Spoof WebGL renderer with Firefox-appropriate Mesa/driver strings. + fmt.Sprintf(`(function() { const getParam = WebGLRenderingContext.prototype.getParameter; WebGLRenderingContext.prototype.getParameter = function(param) { - if (param === 37445) return 'Intel Open Source Technology Center'; - if (param === 37446) return 'Mesa DRI Intel(R) UHD Graphics 630'; + if (param === 37445) return '%s'; + if (param === 37446) return '%s'; return getParam.call(this, param); }; if (typeof WebGL2RenderingContext !== 'undefined') { const getParam2 = WebGL2RenderingContext.prototype.getParameter; WebGL2RenderingContext.prototype.getParameter = function(param) { - if (param === 37445) return 'Intel Open Source Technology Center'; - if (param === 37446) return 'Mesa DRI Intel(R) UHD Graphics 630'; + if (param === 37445) return '%s'; + if (param === 37446) return '%s'; return getParam2.call(this, param); }; } - })()`, + })()`, p.WebGLVendor, p.WebGLRenderer, p.WebGLVendor, p.WebGLRenderer), - // Spoof mozInnerScreenX/mozInnerScreenY which are 0 in headless Firefox. - `(function() { + // Spoof mozInnerScreenX/mozInnerScreenY which are 0 in headless Firefox. + fmt.Sprintf(`(function() { if (window.mozInnerScreenX === 0) { - Object.defineProperty(window, 'mozInnerScreenX', { get: () => 8 }); + Object.defineProperty(window, 'mozInnerScreenX', { get: () => %d }); } if (window.mozInnerScreenY === 0) { - Object.defineProperty(window, 'mozInnerScreenY', { get: () => 51 }); + Object.defineProperty(window, 'mozInnerScreenY', { get: () => %d }); } - })()`, + })()`, p.MozInnerScreenX, p.MozInnerScreenY), - // Normalize navigator.hardwareConcurrency (Firefox headless sometimes reports 2). - `(function() { + // Normalize navigator.hardwareConcurrency (Firefox headless sometimes reports 2). + fmt.Sprintf(`(function() { if (navigator.hardwareConcurrency <= 2) { - Object.defineProperty(navigator, 'hardwareConcurrency', { get: () => 4 }); + Object.defineProperty(navigator, 'hardwareConcurrency', { get: () => %d }); } - })()`, + })()`, p.HardwareConcurrency), - // Override navigator.plugins with Firefox-appropriate PDF.js entry. - `Object.defineProperty(navigator, 'plugins', { + // Override navigator.plugins with Firefox-appropriate PDF.js entry. + `Object.defineProperty(navigator, 'plugins', { get: () => { const arr = [ { name: 'PDF.js', filename: 'internal-pdf-viewer', description: 'Portable Document Format' }, @@ -197,4 +267,5 @@ var stealthFirefoxScripts = []string{ return arr; }, })`, + } } diff --git a/stealth_test.go b/stealth_test.go index 5897079..0375406 100644 --- a/stealth_test.go +++ b/stealth_test.go @@ -129,14 +129,16 @@ func TestStealthCommonScripts_Notification(t *testing.T) { // --- Chromium scripts --- func TestStealthChromiumScripts_Count(t *testing.T) { - if len(stealthChromiumScripts) != 8 { - t.Fatalf("expected 8 chromium stealth scripts, got %d", len(stealthChromiumScripts)) + scripts := buildChromiumStealthScripts(chromiumHWProfiles[0]) + if len(scripts) != 8 { + t.Fatalf("expected 8 chromium stealth scripts, got %d", len(scripts)) } } func TestStealthChromiumScripts_Plugins(t *testing.T) { + scripts := buildChromiumStealthScripts(chromiumHWProfiles[0]) found := false - for _, s := range stealthChromiumScripts { + for _, s := range scripts { if strings.Contains(s, "Chrome PDF Plugin") && strings.Contains(s, "navigator") && strings.Contains(s, "plugins") { found = true break @@ -148,8 +150,9 @@ func TestStealthChromiumScripts_Plugins(t *testing.T) { } func TestStealthChromiumScripts_MimeTypes(t *testing.T) { + scripts := buildChromiumStealthScripts(chromiumHWProfiles[0]) found := false - for _, s := range stealthChromiumScripts { + for _, s := range scripts { if strings.Contains(s, "mimeTypes") && strings.Contains(s, "application/pdf") { found = true break @@ -161,8 +164,9 @@ func TestStealthChromiumScripts_MimeTypes(t *testing.T) { } func TestStealthChromiumScripts_WindowChrome(t *testing.T) { + scripts := buildChromiumStealthScripts(chromiumHWProfiles[0]) found := false - for _, s := range stealthChromiumScripts { + for _, s := range scripts { if strings.Contains(s, "window.chrome") && strings.Contains(s, "runtime") { found = true break @@ -174,8 +178,9 @@ func TestStealthChromiumScripts_WindowChrome(t *testing.T) { } func TestStealthChromiumScripts_ChromeApp(t *testing.T) { + scripts := buildChromiumStealthScripts(chromiumHWProfiles[0]) found := false - for _, s := range stealthChromiumScripts { + for _, s := range scripts { if strings.Contains(s, "chrome.app") && strings.Contains(s, "chrome.csi") && strings.Contains(s, "chrome.loadTimes") { found = true break @@ -187,8 +192,9 @@ func TestStealthChromiumScripts_ChromeApp(t *testing.T) { } func TestStealthChromiumScripts_WebGLSpoof(t *testing.T) { + scripts := buildChromiumStealthScripts(chromiumHWProfiles[0]) found := false - for _, s := range stealthChromiumScripts { + for _, s := range scripts { if strings.Contains(s, "37446") && strings.Contains(s, "ANGLE") { found = true break @@ -200,8 +206,9 @@ func TestStealthChromiumScripts_WebGLSpoof(t *testing.T) { } func TestStealthChromiumScripts_NavigatorConnection(t *testing.T) { + scripts := buildChromiumStealthScripts(chromiumHWProfiles[0]) found := false - for _, s := range stealthChromiumScripts { + for _, s := range scripts { if strings.Contains(s, "connection") && strings.Contains(s, "effectiveType") { found = true break @@ -213,8 +220,9 @@ func TestStealthChromiumScripts_NavigatorConnection(t *testing.T) { } func TestStealthChromiumScripts_CDPCleanup(t *testing.T) { + scripts := buildChromiumStealthScripts(chromiumHWProfiles[0]) found := false - for _, s := range stealthChromiumScripts { + for _, s := range scripts { if strings.Contains(s, "cdc_") && strings.Contains(s, "delete") { found = true break @@ -226,8 +234,9 @@ func TestStealthChromiumScripts_CDPCleanup(t *testing.T) { } func TestStealthChromiumScripts_UserAgentStrip(t *testing.T) { + scripts := buildChromiumStealthScripts(chromiumHWProfiles[0]) found := false - for _, s := range stealthChromiumScripts { + for _, s := range scripts { if strings.Contains(s, "HeadlessChrome") && strings.Contains(s, "userAgent") { found = true break @@ -241,14 +250,16 @@ func TestStealthChromiumScripts_UserAgentStrip(t *testing.T) { // --- Firefox scripts --- func TestStealthFirefoxScripts_Count(t *testing.T) { - if len(stealthFirefoxScripts) != 5 { - t.Fatalf("expected 5 firefox stealth scripts, got %d", len(stealthFirefoxScripts)) + scripts := buildFirefoxStealthScripts(firefoxHWProfiles[0]) + if len(scripts) != 5 { + t.Fatalf("expected 5 firefox stealth scripts, got %d", len(scripts)) } } func TestStealthFirefoxScripts_WebdriverHardening(t *testing.T) { + scripts := buildFirefoxStealthScripts(firefoxHWProfiles[0]) found := false - for _, s := range stealthFirefoxScripts { + for _, s := range scripts { if strings.Contains(s, "getOwnPropertyDescriptor") && strings.Contains(s, "webdriver") { found = true break @@ -260,8 +271,9 @@ func TestStealthFirefoxScripts_WebdriverHardening(t *testing.T) { } func TestStealthFirefoxScripts_WebGLSpoof(t *testing.T) { + scripts := buildFirefoxStealthScripts(firefoxHWProfiles[0]) found := false - for _, s := range stealthFirefoxScripts { + for _, s := range scripts { if strings.Contains(s, "37446") && strings.Contains(s, "Mesa DRI") { found = true break @@ -273,8 +285,9 @@ func TestStealthFirefoxScripts_WebGLSpoof(t *testing.T) { } func TestStealthFirefoxScripts_MozInnerScreen(t *testing.T) { + scripts := buildFirefoxStealthScripts(firefoxHWProfiles[0]) found := false - for _, s := range stealthFirefoxScripts { + for _, s := range scripts { if strings.Contains(s, "mozInnerScreenX") && strings.Contains(s, "mozInnerScreenY") { found = true break @@ -286,8 +299,9 @@ func TestStealthFirefoxScripts_MozInnerScreen(t *testing.T) { } func TestStealthFirefoxScripts_HardwareConcurrency(t *testing.T) { + scripts := buildFirefoxStealthScripts(firefoxHWProfiles[0]) found := false - for _, s := range stealthFirefoxScripts { + for _, s := range scripts { if strings.Contains(s, "hardwareConcurrency") { found = true break @@ -299,8 +313,9 @@ func TestStealthFirefoxScripts_HardwareConcurrency(t *testing.T) { } func TestStealthFirefoxScripts_PDFjsPlugins(t *testing.T) { + scripts := buildFirefoxStealthScripts(firefoxHWProfiles[0]) found := false - for _, s := range stealthFirefoxScripts { + for _, s := range scripts { if strings.Contains(s, "PDF.js") && strings.Contains(s, "plugins") { found = true break @@ -314,17 +329,19 @@ func TestStealthFirefoxScripts_PDFjsPlugins(t *testing.T) { // --- Cross-category validation --- func TestStealthScripts_NoOverlap(t *testing.T) { + chromiumScripts := buildChromiumStealthScripts(chromiumHWProfiles[0]) + firefoxScripts := buildFirefoxStealthScripts(firefoxHWProfiles[0]) all := make(map[string]string) // script -> category for _, s := range stealthCommonScripts { all[s] = "common" } - for _, s := range stealthChromiumScripts { + for _, s := range chromiumScripts { if cat, ok := all[s]; ok { t.Fatalf("chromium script also appears in %s category", cat) } all[s] = "chromium" } - for _, s := range stealthFirefoxScripts { + for _, s := range firefoxScripts { if cat, ok := all[s]; ok { t.Fatalf("firefox script also appears in %s category", cat) } @@ -354,8 +371,9 @@ func TestStealthCommonScripts_NoFirefoxMarkers(t *testing.T) { } func TestStealthChromiumScripts_NoFirefoxMarkers(t *testing.T) { + scripts := buildChromiumStealthScripts(chromiumHWProfiles[0]) firefoxMarkers := []string{"mozInnerScreen", "Mesa DRI", "PDF.js"} - for _, s := range stealthChromiumScripts { + for _, s := range scripts { for _, marker := range firefoxMarkers { if strings.Contains(s, marker) { t.Fatalf("chromium script contains Firefox-specific marker %q", marker) @@ -365,8 +383,9 @@ func TestStealthChromiumScripts_NoFirefoxMarkers(t *testing.T) { } func TestStealthFirefoxScripts_NoChromiumMarkers(t *testing.T) { + scripts := buildFirefoxStealthScripts(firefoxHWProfiles[0]) chromiumMarkers := []string{"window.chrome", "chrome.app", "chrome.csi", "chrome.loadTimes", "HeadlessChrome", "cdc_", "Chrome PDF Plugin", "ANGLE"} - for _, s := range stealthFirefoxScripts { + for _, s := range scripts { for _, marker := range chromiumMarkers { if strings.Contains(s, marker) { t.Fatalf("firefox script contains Chromium-specific marker %q", marker) @@ -439,3 +458,76 @@ func TestMergeOptions_ExplicitUAPreserved(t *testing.T) { t.Fatalf("expected explicit UA %q preserved, got %q", customUA, got.UserAgent) } } + +// --- Hardware profile pools --- + +func TestChromiumHWProfiles_NotEmpty(t *testing.T) { + if len(chromiumHWProfiles) < 2 { + t.Fatalf("expected at least 2 chromium hardware profiles, got %d", len(chromiumHWProfiles)) + } +} + +func TestFirefoxHWProfiles_NotEmpty(t *testing.T) { + if len(firefoxHWProfiles) < 2 { + t.Fatalf("expected at least 2 firefox hardware profiles, got %d", len(firefoxHWProfiles)) + } +} + +func TestBuildChromiumStealthScripts_ProfileValues(t *testing.T) { + p := chromiumHWProfiles[1] // NVIDIA profile + scripts := buildChromiumStealthScripts(p) + joined := strings.Join(scripts, "\n") + if !strings.Contains(joined, p.WebGLVendor) { + t.Fatalf("expected chromium scripts to contain vendor %q", p.WebGLVendor) + } + if !strings.Contains(joined, p.WebGLRenderer) { + t.Fatalf("expected chromium scripts to contain renderer %q", p.WebGLRenderer) + } +} + +func TestBuildFirefoxStealthScripts_ProfileValues(t *testing.T) { + p := firefoxHWProfiles[2] // AMD profile + scripts := buildFirefoxStealthScripts(p) + joined := strings.Join(scripts, "\n") + if !strings.Contains(joined, p.WebGLVendor) { + t.Fatalf("expected firefox scripts to contain vendor %q", p.WebGLVendor) + } + if !strings.Contains(joined, p.WebGLRenderer) { + t.Fatalf("expected firefox scripts to contain renderer %q", p.WebGLRenderer) + } +} + +func TestBuildChromiumStealthScripts_ConnectionJitter(t *testing.T) { + p := chromiumHWProfiles[0] + seen := make(map[string]bool) + for range 50 { + scripts := buildChromiumStealthScripts(p) + // The connection script is at index 5. + seen[scripts[5]] = true + } + if len(seen) < 2 { + t.Fatal("expected connection script to vary across calls due to jitter, but all 50 were identical") + } +} + +func TestChromiumHWProfiles_NoSingleQuotes(t *testing.T) { + for i, p := range chromiumHWProfiles { + if strings.Contains(p.WebGLVendor, "'") { + t.Fatalf("chromium profile %d vendor contains single quote (breaks JS)", i) + } + if strings.Contains(p.WebGLRenderer, "'") { + t.Fatalf("chromium profile %d renderer contains single quote (breaks JS)", i) + } + } +} + +func TestFirefoxHWProfiles_NoSingleQuotes(t *testing.T) { + for i, p := range firefoxHWProfiles { + if strings.Contains(p.WebGLVendor, "'") { + t.Fatalf("firefox profile %d vendor contains single quote (breaks JS)", i) + } + if strings.Contains(p.WebGLRenderer, "'") { + t.Fatalf("firefox profile %d renderer contains single quote (breaks JS)", i) + } + } +} -- 2.49.1