1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
//! Characterisation of the `navigator.webdriver` tell on plain BiDi Firefox.
//!
//! Load-bearing platform fact, proven live (not assumed): on a Firefox driven
//! over BiDi, the active remote agent (which `--remote-debugging-port` activates
//! and which BiDi control requires) sets `navigator.webdriver = true`, and the
//! launch pref `dom.webdriver.enabled=false` does NOT override it. So:
//!
//! - the **native** value cannot be hidden by prefs on plain BiDi — full
//! coverage (Workers / fresh iframes / pre-preload re-reads, i.e. the
//! `webDriverAdvanced` signal) needs the **reynard engine build**, which
//! patches out the automation→`webdriver` coupling;
//! - what we CAN do without the engine is mask the **main world** via the JS
//! getter override in `apply_stealth`.
//!
//! This test pins both facts so a future "just set the pref" attempt is caught
//! immediately (the pref attempt was tried and falsified here). If a Firefox /
//! rustenium change ever makes the native value `false` with the pref, the first
//! assertion flips — that's a gap CLOSING, and worth knowing.
//!
//! Live: requires Firefox. Offline otherwise (navigates only `about:blank`).
use captchaforge::browser::{launch_firefox, FoxBrowserConfig};
fn profile_dir(tag: &str) -> String {
std::env::temp_dir()
.join(format!("cf-webdriver-pref-{}-{}", std::process::id(), tag))
.to_string_lossy()
.to_string()
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn pref_cannot_hide_native_webdriver_but_apply_stealth_masks_main_world() {
let page = launch_firefox(FoxBrowserConfig {
headless: true,
profile_dir: Some(profile_dir("native")),
viewport_width: 1280,
viewport_height: 720,
// The full guise pref set, including dom.webdriver.enabled=false.
user_js_content: Some(captchaforge::stealth::automation_prefs()),
..Default::default()
})
.await
.expect("launch Firefox");
page.goto("about:blank").await.expect("navigate about:blank");
// 1) NATIVE value (no JS override yet): the remote agent forces it true even
// with dom.webdriver.enabled=false. This is the engine-level tell.
let native: String = page
.evaluate("String(navigator.webdriver)")
.await
.expect("evaluate")
.into_value()
.expect("string");
eprintln!("[webdriver] native (pref only) = {native}");
assert_eq!(
native, "true",
"plain BiDi: the remote agent overrides dom.webdriver.enabled — native \
navigator.webdriver stays true (engine/reynard path required to flip it). \
If this is now \"false\", the platform changed and the tell is closing."
);
// 2) After apply_stealth, the MAIN-WORLD value is masked false — the part we
// can do on plain Firefox without the engine build.
captchaforge::apply_stealth(&page)
.await
.expect("apply_stealth");
let masked: String = page
.evaluate("String(navigator.webdriver)")
.await
.expect("evaluate")
.into_value()
.expect("string");
eprintln!("[webdriver] main world (stealthed) = {masked}");
assert!(
masked == "false" || masked == "undefined",
"apply_stealth must mask navigator.webdriver in the main world (got {masked:?})"
);
let _ = page.close().await;
}