fix(stealth): AGENT_BROWSER_DISABLE_IFRAME_PROXY for a clean 0% CreepJS (issue #4)
--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).
This commit is contained in:
@@ -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.)
|
`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)
|
### Human-like input (behavioural stealth)
|
||||||
|
|
||||||
|
|||||||
@@ -51,18 +51,19 @@ pub fn build_stealth_script(mode: StealthMode, locale: Option<&str>) -> String {
|
|||||||
vec![locale, base_lang]
|
vec![locale, base_lang]
|
||||||
};
|
};
|
||||||
let config_line = format!(
|
let config_line = format!(
|
||||||
r#"const __abStealth = {{ locale: "{}", languages: {}, allowWebGLContextFallback: false, hideCanvas: {}, canvasSeed: {} }};"#,
|
r#"const __abStealth = {{ locale: "{}", languages: {}, allowWebGLContextFallback: false, hideCanvas: {}, canvasSeed: {}, disableIframeProxy: {} }};"#,
|
||||||
locale,
|
locale,
|
||||||
serde_json::to_string(&languages).unwrap_or_else(|_| r#"["en-US","en"]"#.to_string()),
|
serde_json::to_string(&languages).unwrap_or_else(|_| r#"["en-US","en"]"#.to_string()),
|
||||||
hide_canvas_enabled(),
|
hide_canvas_enabled(),
|
||||||
canvas_noise_seed(),
|
canvas_noise_seed(),
|
||||||
|
disable_iframe_proxy_enabled(),
|
||||||
);
|
);
|
||||||
|
|
||||||
// NB: this prefix MUST match the first line of stealth_scripts.js verbatim,
|
// NB: this prefix MUST match the first line of stealth_scripts.js verbatim,
|
||||||
// otherwise the fallback below prepends a SECOND `const __abStealth`
|
// otherwise the fallback below prepends a SECOND `const __abStealth`
|
||||||
// declaration and the whole script dies with a redeclaration SyntaxError.
|
// declaration and the whole script dies with a redeclaration SyntaxError.
|
||||||
if let Some(rest) = STEALTH_SCRIPTS_RAW.strip_prefix(
|
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)
|
format!("{}{}", config_line, rest)
|
||||||
} else {
|
} else {
|
||||||
@@ -81,6 +82,18 @@ fn hide_canvas_enabled() -> bool {
|
|||||||
.unwrap_or(false)
|
.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
|
/// 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
|
/// 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".
|
/// headless-stable default. 0 is avoided so the JS can treat it as "unset".
|
||||||
|
|||||||
@@ -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),
|
// Redefine a navigator property on its PROTOTYPE (Navigator / WorkerNavigator),
|
||||||
// the way real Chrome exposes these — as prototype getters, NOT instance own
|
// the way real Chrome exposes these — as prototype getters, NOT instance own
|
||||||
// properties. Adding an own property to the `navigator` instance is itself a
|
// properties. Adding an own property to the `navigator` instance is itself a
|
||||||
@@ -289,6 +289,10 @@ const __abRedefineNavProto = (name, getterImpl) => {
|
|||||||
})();
|
})();
|
||||||
(function(){
|
(function(){
|
||||||
if (typeof document === 'undefined' || typeof document.createElement !== 'function') return;
|
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 nativeCreateElement = document.createElement.bind(document);
|
||||||
const nativeSrcdocDescriptor =
|
const nativeSrcdocDescriptor =
|
||||||
typeof HTMLIFrameElement !== 'undefined'
|
typeof HTMLIFrameElement !== 'undefined'
|
||||||
|
|||||||
Reference in New Issue
Block a user