Skip to content

Commit 4d2feca

Browse files
authored
Merge pull request #282 from XrafACC/pr1
refactor: update stealth test bypass scripts
2 parents e74adcd + 61c759b commit 4d2feca

File tree

7 files changed

+251
-16
lines changed

7 files changed

+251
-16
lines changed

src/browser/config.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,9 @@ impl BrowserConfigBuilder {
328328

329329
pub fn hide(mut self) -> Self {
330330
self.hidden = true;
331+
if self.hidden && self.viewport == Some(Viewport::default()) {
332+
self.viewport = None;
333+
}
331334
self
332335
}
333336

src/handler/viewport.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#[derive(Debug, Clone)]
1+
#[derive(Debug, Clone, PartialEq)]
22
pub struct Viewport {
33
pub width: u32,
44
pub height: u32,

src/page.rs

Lines changed: 64 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -122,17 +122,66 @@ impl Page {
122122
async fn hide_plugins(&self) -> Result<(), CdpError> {
123123
self.execute(AddScriptToEvaluateOnNewDocumentParams {
124124
source: "
125-
Object.defineProperty(
126-
navigator,
127-
'plugins',
128-
{
129-
get: () => [
130-
{ filename: 'internal-pdf-viewer' },
131-
{ filename: 'adsfkjlkjhalkh' },
132-
{ filename: 'internal-nacl-plugin '}
133-
],
125+
// Create a proper PluginArray-like object (NOT an Array!)
126+
// Key insight: PluginArray is array-like but Array.isArray() returns false
127+
const makePlugin = (name, filename, description) => {
128+
const plugin = Object.create(Plugin.prototype);
129+
130+
Object.defineProperties(plugin, {
131+
name: { value: name, enumerable: true },
132+
filename: { value: filename, enumerable: true },
133+
description: { value: description, enumerable: true },
134+
length: { value: 1, enumerable: true },
135+
0: { value: { type: 'application/pdf', suffixes: 'pdf', description }, enumerable: true }
136+
});
137+
return plugin;
138+
};
139+
140+
// Create the fake PluginArray using the real PluginArray prototype
141+
const fakePlugins = Object.create(PluginArray.prototype);
142+
const plugins = [
143+
makePlugin('PDF Viewer', 'internal-pdf-viewer', 'Portable Document Format'),
144+
makePlugin('Chrome PDF Viewer', 'internal-pdf-viewer', 'Portable Document Format'),
145+
makePlugin('Chromium PDF Viewer', 'internal-pdf-viewer', 'Portable Document Format'),
146+
makePlugin('Microsoft Edge PDF Viewer', 'internal-pdf-viewer', 'Portable Document Format'),
147+
makePlugin('WebKit built-in PDF', 'internal-pdf-viewer', 'Portable Document Format')
148+
];
149+
// Add indexed access and length
150+
plugins.forEach((p, i) => {
151+
Object.defineProperty(fakePlugins, i, { value: p, enumerable: true });
152+
});
153+
Object.defineProperty(fakePlugins, 'length', { value: plugins.length, enumerable: true });
154+
// Add methods
155+
Object.defineProperty(fakePlugins, 'item', {
156+
value: function(index) { return this[index] || null; },
157+
enumerable: false
158+
});
159+
Object.defineProperty(fakePlugins, 'namedItem', {
160+
value: function(name) {
161+
for (let i = 0; i < this.length; i++) {
162+
if (this[i].name === name) return this[i];
134163
}
135-
);
164+
return null;
165+
},
166+
enumerable: false
167+
});
168+
169+
Object.defineProperty(fakePlugins, 'refresh', {
170+
value: function() {},
171+
enumerable: false
172+
});
173+
// Make it iterable
174+
Object.defineProperty(fakePlugins, Symbol.iterator, {
175+
value: function* () {
176+
for (let i = 0; i < this.length; i++) yield this[i];
177+
},
178+
enumerable: false
179+
});
180+
181+
Object.defineProperty(Object.getPrototypeOf(navigator), 'plugins', {
182+
get: () => fakePlugins,
183+
configurable: true
184+
});
136185
"
137186
.to_string(),
138187
world_name: None,
@@ -163,14 +212,14 @@ impl Page {
163212
Ok(())
164213
}
165214

166-
/// Removes the `navigator.webdriver` property on frame creation
215+
/// Sets the `navigator.webdriver` property to `false` on frame creation
167216
async fn hide_webdriver(&self) -> Result<(), CdpError> {
168217
self.execute(AddScriptToEvaluateOnNewDocumentParams {
169218
source: "
170219
Object.defineProperty(
171-
navigator,
220+
Object.getPrototypeOf(navigator), //Fixes 'Object.getOwnPropertyNames(navigator) should return empty array'
172221
'webdriver',
173-
{ get: () => undefined }
222+
{ get: () => false } //Fixes 'property should not be undefined'
174223
);
175224
"
176225
.to_string(),
@@ -1219,8 +1268,8 @@ impl Page {
12191268
/// # async fn example(page: Page) -> Result<(), Box<dyn std::error::Error>> {
12201269
/// // Hide webdriver property for stealth scraping
12211270
/// page.evaluate_on_new_document(r#"
1222-
/// Object.defineProperty(navigator, 'webdriver', {
1223-
/// get: () => undefined
1271+
/// Object.defineProperty(Object.getPrototypeOf(navigator), 'webdriver', {
1272+
/// get: () => false
12241273
/// });
12251274
/// "#).await?;
12261275
/// # Ok(())

tests/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use futures::{FutureExt, StreamExt};
66
mod basic;
77
mod config;
88
mod page;
9+
mod stealth;
910

1011
pub async fn test<T>(test: T)
1112
where

tests/stealth/incolumitas.rs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
use std::time::Duration;
2+
3+
use crate::test;
4+
use chromiumoxide::browser::Browser;
5+
use serde_json::Value;
6+
use tokio::time::sleep;
7+
8+
#[tokio::test]
9+
async fn bot_detection() {
10+
test(async |browser: &mut Browser| {
11+
let page = browser.new_page("about:blank").await.unwrap();
12+
page.enable_stealth_mode().await.unwrap();
13+
14+
page.goto("https://bot.incolumitas.com").await.unwrap();
15+
16+
sleep(Duration::from_secs(1)).await; //Wait 1 second to finish the tests
17+
18+
let new_test_raw = page
19+
.find_element("#new-tests")
20+
.await
21+
.unwrap()
22+
.inner_text()
23+
.await
24+
.unwrap()
25+
.unwrap_or_else(|| "{}".to_string());
26+
27+
let new_test_json: Value = serde_json::from_str(&new_test_raw).unwrap();
28+
29+
let old_test_raw = page
30+
.find_element("#detection-tests")
31+
.await
32+
.unwrap()
33+
.inner_text()
34+
.await
35+
.unwrap()
36+
.unwrap_or_else(|| "{}".to_string());
37+
let old_test_json: Value = serde_json::from_str(&old_test_raw).unwrap();
38+
39+
let new_failed: Vec<String> = new_test_json
40+
.as_object()
41+
.unwrap()
42+
.iter()
43+
.filter_map(|(k, v)| if v == "FAIL" { Some(k.clone()) } else { None })
44+
.collect();
45+
assert!(
46+
new_failed.is_empty(),
47+
"New test FAIL: {}",
48+
new_failed.join(", ")
49+
);
50+
51+
let mut old_failed = Vec::new();
52+
for (group, checks) in old_test_json.as_object().unwrap() {
53+
for (key, value) in checks.as_object().unwrap() {
54+
if group == "fpscanner" && key == "WEBDRIVER" {
55+
continue;
56+
} //false alarm
57+
if value == "FAIL" {
58+
old_failed.push(format!("{}.{}", group, key));
59+
}
60+
}
61+
}
62+
assert!(
63+
old_failed.is_empty(),
64+
"Old test FAIL: {}",
65+
old_failed.join(", ")
66+
);
67+
})
68+
.await;
69+
}

tests/stealth/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
pub mod incolumitas;
2+
pub mod rebrowser;

tests/stealth/rebrowser.rs

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
use std::time::Duration;
2+
3+
use chromiumoxide::{BrowserConfig, Page};
4+
use serde::Deserialize;
5+
use tokio::time::sleep;
6+
7+
use crate::test_config;
8+
9+
#[derive(Debug, Deserialize)]
10+
struct DetectionRow {
11+
#[serde(rename = "type")]
12+
kind: String,
13+
14+
rating: f64,
15+
note: String,
16+
}
17+
18+
#[tokio::test]
19+
async fn bot_detection() {
20+
let config = BrowserConfig::builder().hide().build().unwrap(); //needed .hide()
21+
test_config(config, async |browser| {
22+
let page = browser.new_page("about:blank").await.unwrap();
23+
page.enable_stealth_mode().await.unwrap();
24+
25+
page.goto("https://bot-detector.rebrowser.net/")
26+
.await
27+
.unwrap();
28+
test_dummyfn(&page).await;
29+
test_sourceurlleak(&page).await;
30+
//skip mainWorldExecution, because i'm noob
31+
test_runtimeenableleak(&page).await;
32+
test_exposefnleak(&page).await;
33+
test_navigator_webdriver(&page).await;
34+
test_viewport(&page).await;
35+
test_initscripts(&page).await;
36+
test_csp(&page).await;
37+
})
38+
.await;
39+
}
40+
41+
async fn test_csp(page: &Page) {
42+
let result = get_result(page, "bypassCsp").await.unwrap();
43+
assert!(result.rating < 0.0, "{}", result.note)
44+
}
45+
async fn test_initscripts(page: &Page) {
46+
let result = get_result(page, "pwInitScripts").await.unwrap();
47+
assert!(result.rating < 0.0, "{}", result.note)
48+
}
49+
async fn test_viewport(page: &Page) {
50+
let result = get_result(page, "viewport").await.unwrap();
51+
assert!(result.rating < 0.0, "{}", result.note)
52+
}
53+
54+
async fn test_navigator_webdriver(page: &Page) { //Can easliy be broken (because extensions may override)
55+
let result = get_result(page, "navigatorWebdriver").await.unwrap();
56+
assert!(result.rating < 0.0, "{}", result.note)
57+
}
58+
59+
async fn test_exposefnleak(page: &Page) { //Has issue, can be skipped
60+
page.expose_function("exposedFn", "() => { console.log('exposedFn call') }")
61+
.await
62+
.unwrap();
63+
let result = get_result(page, "exposeFunctionLeak").await.unwrap();
64+
assert!(result.rating <= 0.0, "{}", result.note)
65+
}
66+
67+
async fn test_runtimeenableleak(page: &Page) {
68+
let result = get_result(page, "runtimeEnableLeak").await.unwrap();
69+
assert!(result.rating < 0.0, "{}", result.note)
70+
}
71+
async fn test_sourceurlleak(page: &Page) {
72+
page.evaluate("document.getElementById('detections-json')")
73+
.await
74+
.unwrap();
75+
let result = get_result(page, "sourceUrlLeak").await.unwrap();
76+
assert!(result.rating < 0.0, "{}", result.note)
77+
}
78+
79+
async fn test_dummyfn(page: &Page) {
80+
page.evaluate("window.dummyFn()").await.unwrap();
81+
let result = get_result(page, "dummyFn").await.unwrap();
82+
assert!(result.rating < 0.0, "{}", result.note)
83+
}
84+
85+
async fn get_result(page: &Page, target_kind: &str) -> Option<DetectionRow> { //maybe doesnt needed that much effort
86+
let timeout_secs = 15;
87+
let interval = Duration::from_millis(500);
88+
let mut elapsed = Duration::from_secs(0);
89+
90+
loop {
91+
let script = "document.querySelector('#detections-json') ? document.querySelector('#detections-json').value : ''";
92+
if let Ok(Some(js_value)) = page.evaluate(script).await.map(|v| v.into_value::<String>().ok()) {
93+
94+
if !js_value.trim().is_empty() && js_value.starts_with('[') {
95+
if let Ok(list) = serde_json::from_str::<Vec<DetectionRow>>(&js_value) {
96+
if let Some(found) = list.into_iter().find(|p| p.kind == target_kind) {
97+
return Some(found);
98+
}
99+
}
100+
}
101+
}
102+
103+
if elapsed >= Duration::from_secs(timeout_secs) {
104+
println!("DEBUG: {} timed out!", target_kind);
105+
return None;
106+
}
107+
108+
sleep(interval).await;
109+
elapsed += interval;
110+
}
111+
}

0 commit comments

Comments
 (0)