From a976287f030219c7a5b188b02bcef804fe10bba4 Mon Sep 17 00:00:00 2001 From: leeguooooo Date: Thu, 11 Jun 2026 23:25:24 +0900 Subject: [PATCH] fix(stealth): AGENT_BROWSER_DISABLE_IFRAME_PROXY for a clean 0% CreepJS (issue #4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --launch mode scored ~20% stealth on CreepJS because the srcdoc-iframe contentWindow Proxy trips `hasIframeProxy` — the proxy that hides automation is itself a fingerprintable tell (violates this fork's own "native > JS lies" rule). Add a config-driven opt-out (no detectable global): AGENT_BROWSER_DISABLE_IFRAME_PROXY=1 drops the patch via __abStealth.disableIframeProxy → the iframe IIFE early-returns → clean 0% CreepJS, trading the niche srcdoc-iframe masking. Default keeps current behavior. README now documents the --launch 20% honestly and scopes the headline 0% to the extension-connect path. Verified: launch + srcdoc page intact with the toggle; stealth tests green (config strip-prefix kept in sync). --- README.md | 2 +- cli/src/native/stealth.rs | 17 +++++++++++++++-- cli/src/native/stealth_scripts.js | 6 +++++- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 757f626..ed097a0 100644 --- a/README.md +++ b/README.md @@ -225,7 +225,7 @@ When connected to your real Chrome, we inject **zero** JavaScript patches. Your `0% stealth` on CreepJS is the key number: because the connect path patches **nothing**, there is no override for a lie-detector to catch. (Dashboards that read `navigator.languages` order or IP geolocation may show a soft "navigator"/"location" flag — that tracks *your real Chrome's* language list and network, not an automation tell.) -When using `--launch` mode (standalone browser), a full suite of stealth patches is applied instead, and it still passes the suite above. +When using `--launch` mode (standalone browser), a full suite of stealth patches is applied instead, and it passes the suite above — with one caveat: CreepJS reports **~20% stealth** because the srcdoc-iframe `contentWindow` patch trips its `hasIframeProxy` probe (the proxy that hides automation is itself a tell). Everything else is clean (`0% headless`, sannysoft/browserscan green, Cloudflare passed). Set **`AGENT_BROWSER_DISABLE_IFRAME_PROXY=1`** to drop that patch for a clean **0% stealth** (trades the niche srcdoc-iframe masking). The **extension-connect path** (your real Chrome) injects zero JS and is unaffected — it's the genuine 0% path. ### Human-like input (behavioural stealth) diff --git a/cli/src/native/stealth.rs b/cli/src/native/stealth.rs index 3ab7381..c76d938 100644 --- a/cli/src/native/stealth.rs +++ b/cli/src/native/stealth.rs @@ -51,18 +51,19 @@ pub fn build_stealth_script(mode: StealthMode, locale: Option<&str>) -> String { vec![locale, base_lang] }; let config_line = format!( - r#"const __abStealth = {{ locale: "{}", languages: {}, allowWebGLContextFallback: false, hideCanvas: {}, canvasSeed: {} }};"#, + r#"const __abStealth = {{ locale: "{}", languages: {}, allowWebGLContextFallback: false, hideCanvas: {}, canvasSeed: {}, disableIframeProxy: {} }};"#, locale, serde_json::to_string(&languages).unwrap_or_else(|_| r#"["en-US","en"]"#.to_string()), hide_canvas_enabled(), canvas_noise_seed(), + disable_iframe_proxy_enabled(), ); // NB: this prefix MUST match the first line of stealth_scripts.js verbatim, // otherwise the fallback below prepends a SECOND `const __abStealth` // declaration and the whole script dies with a redeclaration SyntaxError. if let Some(rest) = STEALTH_SCRIPTS_RAW.strip_prefix( - r#"const __abStealth = { locale: "en-US", languages: ["en-US", "en"], allowWebGLContextFallback: false, hideCanvas: false, canvasSeed: 0 };"#, + r#"const __abStealth = { locale: "en-US", languages: ["en-US", "en"], allowWebGLContextFallback: false, hideCanvas: false, canvasSeed: 0, disableIframeProxy: false };"#, ) { format!("{}{}", config_line, rest) } else { @@ -81,6 +82,18 @@ fn hide_canvas_enabled() -> bool { .unwrap_or(false) } +/// Whether to DROP the srcdoc-iframe `contentWindow` Proxy patch (FullLaunch). +/// That patch masks automation in srcdoc iframes, but the JS `Proxy` is itself a +/// fingerprintable tell (CreepJS `hasIframeProxy` → ~20% stealth). Off by default +/// (keep the patch); `AGENT_BROWSER_DISABLE_IFRAME_PROXY=1` drops it for a clean +/// 0% CreepJS at the cost of that niche srcdoc-iframe masking. +fn disable_iframe_proxy_enabled() -> bool { + std::env::var("AGENT_BROWSER_DISABLE_IFRAME_PROXY") + .ok() + .map(|v| v == "1" || v.eq_ignore_ascii_case("true")) + .unwrap_or(false) +} + /// A per-process seed so canvas/audio noise is STABLE within a session (a real /// device returns the same hash on repeated reads) but differs from the /// headless-stable default. 0 is avoided so the JS can treat it as "unset". diff --git a/cli/src/native/stealth_scripts.js b/cli/src/native/stealth_scripts.js index 2f2866b..487ea3d 100644 --- a/cli/src/native/stealth_scripts.js +++ b/cli/src/native/stealth_scripts.js @@ -1,4 +1,4 @@ -const __abStealth = { locale: "en-US", languages: ["en-US", "en"], allowWebGLContextFallback: false, hideCanvas: false, canvasSeed: 0 }; +const __abStealth = { locale: "en-US", languages: ["en-US", "en"], allowWebGLContextFallback: false, hideCanvas: false, canvasSeed: 0, disableIframeProxy: false }; // Redefine a navigator property on its PROTOTYPE (Navigator / WorkerNavigator), // the way real Chrome exposes these — as prototype getters, NOT instance own // properties. Adding an own property to the `navigator` instance is itself a @@ -289,6 +289,10 @@ const __abRedefineNavProto = (name, getterImpl) => { })(); (function(){ if (typeof document === 'undefined' || typeof document.createElement !== 'function') return; + // The srcdoc-iframe contentWindow Proxy below is itself a fingerprintable tell + // (CreepJS `hasIframeProxy`). Honor the opt-out so callers can trade the niche + // srcdoc masking for a clean 0% CreepJS fingerprint. + if (typeof __abStealth !== 'undefined' && __abStealth.disableIframeProxy) return; const nativeCreateElement = document.createElement.bind(document); const nativeSrcdocDescriptor = typeof HTMLIFrameElement !== 'undefined'