Skip to content

Commit 56268a2

Browse files
copyleftdevclaude
andcommitted
fix(fetch): make navigator.webdriver value an explicit config choice
Addresses Law 1 concern: the webdriver patch value was hardcoded to `undefined`, which is an implicit behavior choice that fails Rebrowser while passing other detectors. Introduces WebdriverValue enum (False | Undefined) on BrowserFetchConfig: - False (default): matches real non-automated Chrome behavior - Undefined: property appears deleted, passes classic detectors Default changed from `undefined` to `false`. This is now an explicit, auditable config input — not hidden detection-driven logic. Live test results after fix: - Rebrowser: 10/10 PASS (was 9/10, webdriver now passes) - Sannysoft: 55/56 PASS (PluginArray prototype, unchanged) - All 5 stealth tests green Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent a408f5f commit 56268a2

2 files changed

Lines changed: 33 additions & 3 deletions

File tree

crates/palimpsest-fetch/src/browser.rs

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,24 @@ use url::Url;
3333

3434
use crate::result::FetchResult;
3535

36+
/// What value `navigator.webdriver` should report in stealth mode.
37+
///
38+
/// Real Chrome returns `false` when not under automation. Some older
39+
/// detectors flag `false` as suspicious (they expect `true` for
40+
/// headless). Others flag `undefined` as "manually deleted." This
41+
/// is an explicit, auditable config choice — not hidden behavior.
42+
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
43+
pub enum WebdriverValue {
44+
/// Return `false` — matches real non-automated Chrome.
45+
/// Passes Rebrowser, Sannysoft, BotD. May trigger older heuristics
46+
/// that specifically check `=== false`.
47+
#[default]
48+
False,
49+
/// Return `undefined` — property appears deleted.
50+
/// Passes most classic detectors but Rebrowser flags as "manually deleted."
51+
Undefined,
52+
}
53+
3654
/// Configuration for the browser fetcher.
3755
#[derive(Debug, Clone)]
3856
pub struct BrowserFetchConfig {
@@ -46,6 +64,9 @@ pub struct BrowserFetchConfig {
4664
/// viewport parity, and permissions. All noise is seeded from
4765
/// CrawlSeed for determinism (Law 1).
4866
pub stealth: bool,
67+
/// What `navigator.webdriver` should return in stealth mode.
68+
/// Default: `False` (matches real Chrome behavior).
69+
pub webdriver_value: WebdriverValue,
4970
}
5071

5172
impl Default for BrowserFetchConfig {
@@ -57,6 +78,7 @@ impl Default for BrowserFetchConfig {
5778
js_enabled: true,
5879
user_agent: "PalimpsestBot/0.1".into(),
5980
stealth: false,
81+
webdriver_value: WebdriverValue::default(),
6082
}
6183
}
6284
}
@@ -575,6 +597,12 @@ fn build_stealth_script(
575597
let vh = config.viewport_height;
576598
let ua = &config.user_agent;
577599

600+
// Webdriver value from config (Law 1: explicit, auditable choice).
601+
let webdriver_js = match config.webdriver_value {
602+
WebdriverValue::False => "false",
603+
WebdriverValue::Undefined => "undefined",
604+
};
605+
578606
// Derive sub-seeds for noise patches (Law 1: deterministic).
579607
let canvas_seed = seed_value.wrapping_mul(0x9E3779B97F4A7C15).wrapping_add(1);
580608
let audio_seed = seed_value.wrapping_mul(0x9E3779B97F4A7C15).wrapping_add(2);
@@ -583,9 +611,10 @@ fn build_stealth_script(
583611
let stealth = format!(
584612
r#"
585613
// === STEALTH PATCH 1: navigator.webdriver ===
586-
// Set to undefined on the prototype, not false (detectors flag false).
614+
// Value is an explicit config choice (WebdriverValue enum), not hidden.
615+
// False = real Chrome behavior. Undefined = property deleted.
587616
Object.defineProperty(Navigator.prototype, 'webdriver', {{
588-
get: () => undefined,
617+
get: () => {webdriver_js},
589618
configurable: true
590619
}});
591620

crates/palimpsest-fetch/tests/stealth_test.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,8 +127,9 @@ fn stealth_script(seed: CrawlSeed) -> String {
127127
}}
128128
129129
// === STEALTH ===
130+
// Value: false matches real non-automated Chrome.
130131
Object.defineProperty(Navigator.prototype, 'webdriver', {{
131-
get: () => undefined,
132+
get: () => false,
132133
configurable: true
133134
}});
134135

0 commit comments

Comments
 (0)