fix: split stealth scripts by browser engine, add Firefox stealth #72

Merged
steve merged 1 commits from fix/69-firefox-stealth-scripts into main 2026-02-24 01:23:25 +00:00
3 changed files with 378 additions and 100 deletions
Showing only changes of commit 34161209de - Show all commits

View File

@@ -61,7 +61,13 @@ func initBrowser(opt BrowserOptions) (*browserInitResult, error) {
if opt.Browser == BrowserChromium { if opt.Browser == BrowserChromium {
launchArgs = append(launchArgs, stealthChromiumArgs...) launchArgs = append(launchArgs, stealthChromiumArgs...)
} }
initScripts = append(initScripts, stealthInitScripts...) initScripts = append(initScripts, stealthCommonScripts...)
switch opt.Browser {
case BrowserChromium:
initScripts = append(initScripts, stealthChromiumScripts...)
case BrowserFirefox:
initScripts = append(initScripts, stealthFirefoxScripts...)
}
} }
launchArgs = append(launchArgs, opt.LaunchArgs...) launchArgs = append(launchArgs, opt.LaunchArgs...)

View File

@@ -5,81 +5,17 @@ var stealthChromiumArgs = []string{
"--disable-blink-features=AutomationControlled", "--disable-blink-features=AutomationControlled",
} }
// stealthInitScripts are JavaScript snippets injected before page scripts to mask automation signals. // stealthCommonScripts are JavaScript snippets injected before page scripts on all browser engines.
var stealthInitScripts = []string{ var stealthCommonScripts = []string{
// Override navigator.webdriver to return undefined (the real-browser value). // Override navigator.webdriver to return undefined (the real-browser value).
`Object.defineProperty(navigator, 'webdriver', {get: () => undefined})`, `Object.defineProperty(navigator, 'webdriver', {get: () => undefined})`,
// Populate navigator.plugins with a realistic entry so plugins.length > 0.
`Object.defineProperty(navigator, 'plugins', {
get: () => {
const arr = [
{ name: 'PDF Viewer', filename: 'internal-pdf-viewer', description: 'Portable Document Format' },
{ name: 'Chrome PDF Plugin', filename: 'internal-pdf-viewer', description: '' },
{ name: 'Chromium PDF Viewer', filename: 'internal-pdf-viewer', description: '' },
];
arr.item = (i) => arr[i] || null;
arr.namedItem = (n) => arr.find(p => p.name === n) || null;
arr.refresh = () => {};
return arr;
},
})`,
// Populate navigator.mimeTypes to match the fake plugins above.
`Object.defineProperty(navigator, 'mimeTypes', {
get: () => {
const arr = [
{ type: 'application/pdf', suffixes: 'pdf', description: 'Portable Document Format' },
];
arr.item = (i) => arr[i] || null;
arr.namedItem = (n) => arr.find(m => m.type === n) || null;
return arr;
},
})`,
// Provide window.chrome runtime stub (Chromium-only signal; harmless on other engines).
`if (!window.chrome) {
window.chrome = { runtime: {} };
}`,
// Fix outerWidth/outerHeight which are 0 in headless mode. // Fix outerWidth/outerHeight which are 0 in headless mode.
`if (window.outerWidth === 0) { `if (window.outerWidth === 0) {
Object.defineProperty(window, 'outerWidth', { get: () => window.innerWidth }); Object.defineProperty(window, 'outerWidth', { get: () => window.innerWidth });
Object.defineProperty(window, 'outerHeight', { get: () => window.innerHeight }); Object.defineProperty(window, 'outerHeight', { get: () => window.innerHeight });
}`, }`,
// Spoof WebGL renderer to hide SwiftShader (headless GPU) fingerprint.
`(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)';
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)';
return getParam2.call(this, param);
};
}
})()`,
// 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' } };
}
if (!window.chrome.csi) {
window.chrome.csi = function() { return { startE: Date.now(), onloadT: Date.now(), pageT: 0, 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, startLoadTime: Date.now() / 1000, wasAlternateProtocolAvailable: false, wasFetchedViaSpdy: true, wasNpnNegotiated: true }; };
}
})()`,
// Override navigator.permissions.query to return "denied" for notifications. // Override navigator.permissions.query to return "denied" for notifications.
`(function() { `(function() {
if (navigator.permissions && navigator.permissions.query) { if (navigator.permissions && navigator.permissions.query) {
@@ -101,8 +37,75 @@ var stealthInitScripts = []string{
Notification.requestPermission = function() { return Promise.resolve('denied'); }; Notification.requestPermission = function() { return Promise.resolve('denied'); };
} }
})()`, })()`,
}
// Stub navigator.connection (Network Information API) if missing. // 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', {
get: () => {
const arr = [
{ name: 'PDF Viewer', filename: 'internal-pdf-viewer', description: 'Portable Document Format' },
{ name: 'Chrome PDF Plugin', filename: 'internal-pdf-viewer', description: '' },
{ name: 'Chromium PDF Viewer', filename: 'internal-pdf-viewer', description: '' },
];
arr.item = (i) => arr[i] || null;
arr.namedItem = (n) => arr.find(p => p.name === n) || null;
arr.refresh = () => {};
return arr;
},
})`,
// 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' },
];
arr.item = (i) => arr[i] || null;
arr.namedItem = (n) => arr.find(m => m.type === n) || null;
return arr;
},
})`,
// 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() {
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' } };
}
if (!window.chrome.csi) {
window.chrome.csi = function() { return { startE: Date.now(), onloadT: Date.now(), pageT: 0, 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, startLoadTime: Date.now() / 1000, wasAlternateProtocolAvailable: false, wasFetchedViaSpdy: true, wasNpnNegotiated: true }; };
}
})()`,
// Spoof WebGL renderer to hide SwiftShader (headless GPU) fingerprint with Chromium ANGLE strings.
`(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)';
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)';
return getParam2.call(this, param);
};
}
})()`,
// Stub navigator.connection (Network Information API) if missing (Chrome-only API).
`(function() { `(function() {
if (!navigator.connection) { if (!navigator.connection) {
Object.defineProperty(navigator, 'connection', { Object.defineProperty(navigator, 'connection', {
@@ -132,3 +135,66 @@ var stealthInitScripts = []string{
} }
})()`, })()`,
} }
// stealthFirefoxScripts are JavaScript snippets specific to Firefox.
var stealthFirefoxScripts = []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) {
if ((obj === navigator || obj === proto) && prop === 'webdriver') {
return undefined;
}
return origGetOwnPropDesc.call(this, obj, prop);
};
})()`,
// Spoof WebGL renderer with Firefox-appropriate Mesa/Intel strings.
`(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';
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';
return getParam2.call(this, param);
};
}
})()`,
// Spoof mozInnerScreenX/mozInnerScreenY which are 0 in headless Firefox.
`(function() {
if (window.mozInnerScreenX === 0) {
Object.defineProperty(window, 'mozInnerScreenX', { get: () => 8 });
}
if (window.mozInnerScreenY === 0) {
Object.defineProperty(window, 'mozInnerScreenY', { get: () => 51 });
}
})()`,
// Normalize navigator.hardwareConcurrency (Firefox headless sometimes reports 2).
`(function() {
if (navigator.hardwareConcurrency <= 2) {
Object.defineProperty(navigator, 'hardwareConcurrency', { get: () => 4 });
}
})()`,
// 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' },
];
arr.item = (i) => arr[i] || null;
arr.namedItem = (n) => arr.find(p => p.name === n) || null;
arr.refresh = () => {};
return arr;
},
})`,
}

View File

@@ -66,105 +66,311 @@ func TestStealthChromiumArgs(t *testing.T) {
} }
} }
func TestStealthInitScripts(t *testing.T) { // --- Common scripts ---
if len(stealthInitScripts) == 0 {
t.Fatal("expected at least one stealth init script") func TestStealthCommonScripts_Count(t *testing.T) {
if len(stealthCommonScripts) != 4 {
t.Fatalf("expected 4 common stealth scripts, got %d", len(stealthCommonScripts))
} }
} }
func TestStealthInitScripts_Count(t *testing.T) { func TestStealthCommonScripts_WebdriverOverride(t *testing.T) {
if len(stealthInitScripts) != 12 {
t.Fatalf("expected 12 stealth init scripts, got %d", len(stealthInitScripts))
}
}
func TestStealthInitScripts_WebGLSpoof(t *testing.T) {
found := false found := false
for _, s := range stealthInitScripts { for _, s := range stealthCommonScripts {
if strings.Contains(s, "SwiftShader") || strings.Contains(s, "UNMASKED_RENDERER") || strings.Contains(s, "37446") { if strings.Contains(s, "navigator") && strings.Contains(s, "webdriver") {
found = true found = true
break break
} }
} }
if !found { if !found {
t.Fatal("expected a stealth script that spoofs WebGL renderer") t.Fatal("expected a common script that overrides navigator.webdriver")
} }
} }
func TestStealthInitScripts_ChromeApp(t *testing.T) { func TestStealthCommonScripts_OuterDimensions(t *testing.T) {
found := false found := false
for _, s := range stealthInitScripts { for _, s := range stealthCommonScripts {
if strings.Contains(s, "chrome.app") && strings.Contains(s, "chrome.csi") && strings.Contains(s, "chrome.loadTimes") { if strings.Contains(s, "outerWidth") && strings.Contains(s, "outerHeight") {
found = true found = true
break break
} }
} }
if !found { if !found {
t.Fatal("expected a stealth script that stubs chrome.app, chrome.csi, and chrome.loadTimes") t.Fatal("expected a common script that fixes outerWidth/outerHeight")
} }
} }
func TestStealthInitScripts_PermissionsQuery(t *testing.T) { func TestStealthCommonScripts_PermissionsQuery(t *testing.T) {
found := false found := false
for _, s := range stealthInitScripts { for _, s := range stealthCommonScripts {
if strings.Contains(s, "permissions.query") && strings.Contains(s, "notifications") { if strings.Contains(s, "permissions.query") && strings.Contains(s, "notifications") {
found = true found = true
break break
} }
} }
if !found { if !found {
t.Fatal("expected a stealth script that overrides permissions.query for notifications") t.Fatal("expected a common script that overrides permissions.query for notifications")
} }
} }
func TestStealthInitScripts_Notification(t *testing.T) { func TestStealthCommonScripts_Notification(t *testing.T) {
found := false found := false
for _, s := range stealthInitScripts { for _, s := range stealthCommonScripts {
if strings.Contains(s, "Notification") && strings.Contains(s, "requestPermission") { if strings.Contains(s, "Notification") && strings.Contains(s, "requestPermission") {
found = true found = true
break break
} }
} }
if !found { if !found {
t.Fatal("expected a stealth script that stubs Notification constructor") t.Fatal("expected a common script that stubs Notification constructor")
} }
} }
func TestStealthInitScripts_NavigatorConnection(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))
}
}
func TestStealthChromiumScripts_Plugins(t *testing.T) {
found := false found := false
for _, s := range stealthInitScripts { for _, s := range stealthChromiumScripts {
if strings.Contains(s, "Chrome PDF Plugin") && strings.Contains(s, "navigator") && strings.Contains(s, "plugins") {
found = true
break
}
}
if !found {
t.Fatal("expected a chromium script that populates navigator.plugins with Chrome entries")
}
}
func TestStealthChromiumScripts_MimeTypes(t *testing.T) {
found := false
for _, s := range stealthChromiumScripts {
if strings.Contains(s, "mimeTypes") && strings.Contains(s, "application/pdf") {
found = true
break
}
}
if !found {
t.Fatal("expected a chromium script that populates navigator.mimeTypes")
}
}
func TestStealthChromiumScripts_WindowChrome(t *testing.T) {
found := false
for _, s := range stealthChromiumScripts {
if strings.Contains(s, "window.chrome") && strings.Contains(s, "runtime") {
found = true
break
}
}
if !found {
t.Fatal("expected a chromium script that stubs window.chrome")
}
}
func TestStealthChromiumScripts_ChromeApp(t *testing.T) {
found := false
for _, s := range stealthChromiumScripts {
if strings.Contains(s, "chrome.app") && strings.Contains(s, "chrome.csi") && strings.Contains(s, "chrome.loadTimes") {
found = true
break
}
}
if !found {
t.Fatal("expected a chromium script that stubs chrome.app, chrome.csi, and chrome.loadTimes")
}
}
func TestStealthChromiumScripts_WebGLSpoof(t *testing.T) {
found := false
for _, s := range stealthChromiumScripts {
if strings.Contains(s, "37446") && strings.Contains(s, "ANGLE") {
found = true
break
}
}
if !found {
t.Fatal("expected a chromium script that spoofs WebGL renderer with ANGLE strings")
}
}
func TestStealthChromiumScripts_NavigatorConnection(t *testing.T) {
found := false
for _, s := range stealthChromiumScripts {
if strings.Contains(s, "connection") && strings.Contains(s, "effectiveType") { if strings.Contains(s, "connection") && strings.Contains(s, "effectiveType") {
found = true found = true
break break
} }
} }
if !found { if !found {
t.Fatal("expected a stealth script that stubs navigator.connection") t.Fatal("expected a chromium script that stubs navigator.connection")
} }
} }
func TestStealthInitScripts_CDPCleanup(t *testing.T) { func TestStealthChromiumScripts_CDPCleanup(t *testing.T) {
found := false found := false
for _, s := range stealthInitScripts { for _, s := range stealthChromiumScripts {
if strings.Contains(s, "cdc_") && strings.Contains(s, "delete") { if strings.Contains(s, "cdc_") && strings.Contains(s, "delete") {
found = true found = true
break break
} }
} }
if !found { if !found {
t.Fatal("expected a stealth script that cleans up CDP artifacts") t.Fatal("expected a chromium script that cleans up CDP artifacts")
} }
} }
func TestStealthInitScripts_UserAgentStrip(t *testing.T) { func TestStealthChromiumScripts_UserAgentStrip(t *testing.T) {
found := false found := false
for _, s := range stealthInitScripts { for _, s := range stealthChromiumScripts {
if strings.Contains(s, "HeadlessChrome") && strings.Contains(s, "userAgent") { if strings.Contains(s, "HeadlessChrome") && strings.Contains(s, "userAgent") {
found = true found = true
break break
} }
} }
if !found { if !found {
t.Fatal("expected a stealth script that strips HeadlessChrome from user agent") t.Fatal("expected a chromium script that strips HeadlessChrome from user agent")
}
}
// --- Firefox scripts ---
func TestStealthFirefoxScripts_Count(t *testing.T) {
if len(stealthFirefoxScripts) != 5 {
t.Fatalf("expected 5 firefox stealth scripts, got %d", len(stealthFirefoxScripts))
}
}
func TestStealthFirefoxScripts_WebdriverHardening(t *testing.T) {
found := false
for _, s := range stealthFirefoxScripts {
if strings.Contains(s, "getOwnPropertyDescriptor") && strings.Contains(s, "webdriver") {
found = true
break
}
}
if !found {
t.Fatal("expected a firefox script that hardens navigator.webdriver via getOwnPropertyDescriptor")
}
}
func TestStealthFirefoxScripts_WebGLSpoof(t *testing.T) {
found := false
for _, s := range stealthFirefoxScripts {
if strings.Contains(s, "37446") && strings.Contains(s, "Mesa DRI") {
found = true
break
}
}
if !found {
t.Fatal("expected a firefox script that spoofs WebGL renderer with Mesa strings")
}
}
func TestStealthFirefoxScripts_MozInnerScreen(t *testing.T) {
found := false
for _, s := range stealthFirefoxScripts {
if strings.Contains(s, "mozInnerScreenX") && strings.Contains(s, "mozInnerScreenY") {
found = true
break
}
}
if !found {
t.Fatal("expected a firefox script that spoofs mozInnerScreenX/mozInnerScreenY")
}
}
func TestStealthFirefoxScripts_HardwareConcurrency(t *testing.T) {
found := false
for _, s := range stealthFirefoxScripts {
if strings.Contains(s, "hardwareConcurrency") {
found = true
break
}
}
if !found {
t.Fatal("expected a firefox script that normalizes navigator.hardwareConcurrency")
}
}
func TestStealthFirefoxScripts_PDFjsPlugins(t *testing.T) {
found := false
for _, s := range stealthFirefoxScripts {
if strings.Contains(s, "PDF.js") && strings.Contains(s, "plugins") {
found = true
break
}
}
if !found {
t.Fatal("expected a firefox script that provides PDF.js plugin entry")
}
}
// --- Cross-category validation ---
func TestStealthScripts_NoOverlap(t *testing.T) {
all := make(map[string]string) // script -> category
for _, s := range stealthCommonScripts {
all[s] = "common"
}
for _, s := range stealthChromiumScripts {
if cat, ok := all[s]; ok {
t.Fatalf("chromium script also appears in %s category", cat)
}
all[s] = "chromium"
}
for _, s := range stealthFirefoxScripts {
if cat, ok := all[s]; ok {
t.Fatalf("firefox script also appears in %s category", cat)
}
}
}
func TestStealthCommonScripts_NoChromiumMarkers(t *testing.T) {
chromiumMarkers := []string{"window.chrome", "chrome.app", "chrome.csi", "chrome.loadTimes", "HeadlessChrome", "cdc_", "Chrome PDF Plugin", "ANGLE"}
for _, s := range stealthCommonScripts {
for _, marker := range chromiumMarkers {
if strings.Contains(s, marker) {
t.Fatalf("common script contains Chromium-specific marker %q", marker)
}
}
}
}
func TestStealthCommonScripts_NoFirefoxMarkers(t *testing.T) {
firefoxMarkers := []string{"mozInnerScreen", "Mesa DRI", "PDF.js"}
for _, s := range stealthCommonScripts {
for _, marker := range firefoxMarkers {
if strings.Contains(s, marker) {
t.Fatalf("common script contains Firefox-specific marker %q", marker)
}
}
}
}
func TestStealthChromiumScripts_NoFirefoxMarkers(t *testing.T) {
firefoxMarkers := []string{"mozInnerScreen", "Mesa DRI", "PDF.js"}
for _, s := range stealthChromiumScripts {
for _, marker := range firefoxMarkers {
if strings.Contains(s, marker) {
t.Fatalf("chromium script contains Firefox-specific marker %q", marker)
}
}
}
}
func TestStealthFirefoxScripts_NoChromiumMarkers(t *testing.T) {
chromiumMarkers := []string{"window.chrome", "chrome.app", "chrome.csi", "chrome.loadTimes", "HeadlessChrome", "cdc_", "Chrome PDF Plugin", "ANGLE"}
for _, s := range stealthFirefoxScripts {
for _, marker := range chromiumMarkers {
if strings.Contains(s, marker) {
t.Fatalf("firefox script contains Chromium-specific marker %q", marker)
}
}
} }
} }