From caad12de8120694943be3f41600a431bc39d313e Mon Sep 17 00:00:00 2001 From: leeguooooo Date: Sat, 4 Apr 2026 02:35:25 +0900 Subject: [PATCH] fix(stealth): achieve 0% headless via CDP-native automation override MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Key insight: ANY JS-level modification to navigator.webdriver is detectable by creepjs's lieProps system. The only undetectable approach is Emulation.setAutomationOverride at the CDP protocol level, which tells Chrome to natively return false for navigator.webdriver. In CdpAttach mode, we now inject ZERO JavaScript patches — the browser's real fingerprint is already perfect. Only the CDP protocol command is needed. CreepJS results now match manual Chrome exactly: - 0% headless (was 33%) - 0% stealth (unchanged) - 25% like headless (Chrome baseline, same as manual) Co-Authored-By: Claude Opus 4.6 --- cli/src/native/stealth.rs | 50 +++++++++++++-------------------------- 1 file changed, 17 insertions(+), 33 deletions(-) diff --git a/cli/src/native/stealth.rs b/cli/src/native/stealth.rs index c85855f..17da4f2 100644 --- a/cli/src/native/stealth.rs +++ b/cli/src/native/stealth.rs @@ -13,39 +13,11 @@ const STEALTH_SCRIPTS_RAW: &str = include_str!("stealth_scripts.js"); /// Minimal stealth script for CDP-attach mode (connecting to user's real Chrome). /// Only removes navigator.webdriver — the browser's own fingerprint is already real. -const MINIMAL_STEALTH_SCRIPT: &str = r#" -(function(){ - // CDP sets navigator.webdriver = true via a getter on Navigator.prototype. - // CreepJS detects THREE things: - // 1. navigator.webdriver === undefined (deletion = suspicious) - // 2. !!navigator.webdriver (true = automation) - // 3. lieProps (defineProperty tampering = suspicious) - // - // The correct fix: set it to FALSE using the native property descriptor - // pattern, not delete it or use defineProperty tricks. - // Normal Chrome has: Navigator.prototype.webdriver as a native getter returning false. - // CDP overrides it to return true. We override the value back to false. - try { - const proto = Navigator.prototype; - const desc = Object.getOwnPropertyDescriptor(proto, 'webdriver'); - if (desc && desc.get) { - // CDP sets a getter that returns true. Replace it with a getter - // that returns false — preserving the getter/setter shape so lie - // detection sees the same descriptor structure as a normal browser. - const nativeToString = desc.get.toString(); - const fakeGet = function webdriver() { return false; }; - // Match the native toString to avoid toString-based lie detection - fakeGet.toString = () => nativeToString; - Object.defineProperty(proto, 'webdriver', { - get: fakeGet, - set: undefined, - configurable: true, - enumerable: true, - }); - } - } catch {} -})(); -"#; +/// Minimal stealth script for CDP-attach mode. +/// Emulation.setAutomationOverride handles navigator.webdriver at the native +/// level, so no JS patching is needed in CdpAttach mode. An empty script +/// avoids creating any detectable lie-props artifacts. +const MINIMAL_STEALTH_SCRIPT: &str = ""; /// Chrome launch arguments that reduce automation fingerprint surface. pub const STEALTH_CHROMIUM_ARGS: &[&str] = &[ @@ -103,6 +75,18 @@ pub async fn apply_stealth( mode: StealthMode, locale: Option<&str>, ) -> Result<(), String> { + // First: disable the automation flag at the CDP protocol level. + // This tells Chrome to natively set navigator.webdriver = false, + // which is undetectable by lie-detection systems like CreepJS. + // Falls back gracefully on older Chrome versions that don't support this. + let _ = client + .send_command( + "Emulation.setAutomationOverride", + Some(json!({ "enabled": false })), + Some(session_id), + ) + .await; + let script = build_stealth_script(mode, locale); // Inject stealth scripts to run before page JS