From 51c7ee0e2c98b6766ed14a362f4f5f37e4990f33 Mon Sep 17 00:00:00 2001 From: leeguooooo Date: Sat, 4 Apr 2026 02:25:22 +0900 Subject: [PATCH] fix(stealth): use getter-based webdriver override to match native Chrome shape MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CreepJS detects three things for webDriverIsOn: 1. Property deletion (navigator.webdriver === undefined) 2. Value check (!!navigator.webdriver) 3. Lie detection (descriptor tampering via lieProps) Changed from delete/defineProperty-value approach to replacing the CDP getter with a getter returning false, matching the native descriptor shape. Note: 33% headless in CreepJS is a CDP-inherent signal (lieProps detects the getter replacement). This cannot be eliminated at the JS layer since CDP sets the webdriver getter before init scripts run. Real-world impact is minimal — Cloudflare Turnstile passes successfully. Also confirmed: Chrome's remote_debugging preference in Local State persists across restarts, so users only need to enable CDP once via chrome://inspect/#remote-debugging. Co-Authored-By: Claude Opus 4.6 --- cli/src/native/stealth.rs | 42 +++++++++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/cli/src/native/stealth.rs b/cli/src/native/stealth.rs index 504213b..c85855f 100644 --- a/cli/src/native/stealth.rs +++ b/cli/src/native/stealth.rs @@ -15,23 +15,35 @@ const STEALTH_SCRIPTS_RAW: &str = include_str!("stealth_scripts.js"); /// Only removes navigator.webdriver — the browser's own fingerprint is already real. const MINIMAL_STEALTH_SCRIPT: &str = r#" (function(){ - // CDP sets a getter on Navigator.prototype.webdriver that returns true. - // Simple `delete` won't remove it. We must redefine the property with - // Object.defineProperty to fully hide it from `'webdriver' in navigator`. - const targets = [Navigator.prototype]; - if (typeof WorkerNavigator !== 'undefined') targets.push(WorkerNavigator.prototype); - targets.push(Object.getPrototypeOf(navigator)); - for (const target of targets) { - if (!target) continue; - try { delete target.webdriver; } catch {} - try { - Object.defineProperty(target, 'webdriver', { - get: undefined, + // 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, }); - delete target.webdriver; - } catch {} - } + } + } catch {} })(); "#;