fix: randomize hardware fingerprints across sessions #74

Merged
steve merged 1 commits from feature/71-randomize-fingerprints into main 2026-02-24 01:39:27 +00:00
3 changed files with 233 additions and 70 deletions
Showing only changes of commit 4772b153b8 - Show all commits

View File

@@ -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())...)
}
}

View File

@@ -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,8 +44,45 @@ var stealthCommonScripts = []string{
})()`,
}
// stealthChromiumScripts are JavaScript snippets specific to Chromium-based browsers.
var stealthChromiumScripts = []string{
// 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: () => {
@@ -88,33 +130,33 @@ var stealthChromiumScripts = []string{
})()`,
// Spoof WebGL renderer to hide SwiftShader (headless GPU) fingerprint with Chromium ANGLE strings.
`(function() {
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() {
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() {
@@ -135,9 +177,37 @@ var stealthChromiumScripts = []string{
}
})()`,
}
}
// stealthFirefoxScripts are JavaScript snippets specific to Firefox.
var stealthFirefoxScripts = []string{
// 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);
@@ -150,40 +220,40 @@ 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() {
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() {
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', {
@@ -198,3 +268,4 @@ var stealthFirefoxScripts = []string{
},
})`,
}
}

View File

@@ -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)
}
}
}