1use serde::{Deserialize, Serialize};
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct CdpHardeningConfig {
45 pub enabled: bool,
47 pub sanitize_stacks: bool,
49 pub protect_console: bool,
51}
52
53impl Default for CdpHardeningConfig {
54 fn default() -> Self {
56 Self {
57 enabled: true,
58 sanitize_stacks: true,
59 protect_console: true,
60 }
61 }
62}
63
64#[must_use]
84#[allow(clippy::too_many_lines)]
85pub fn cdp_hardening_script(config: &CdpHardeningConfig) -> String {
86 if !config.enabled {
87 return String::new();
88 }
89
90 let stack_section = if config.sanitize_stacks {
91 ERROR_STACK_SECTION
92 } else {
93 ""
94 };
95
96 let console_section = if config.protect_console {
97 CONSOLE_DEBUG_SECTION
98 } else {
99 ""
100 };
101
102 format!(
103 r"(function() {{
104 'use strict';
105
106 // ── 1. Delete Playwright / Puppeteer binding remnants ─────────────────
107 var _cdpArtifacts = [
108 '__playwright__binding__',
109 '__pwInitScripts',
110 '__playwright_evaluation_script__',
111 '__puppeteer_evaluation_script__',
112 '__puppeteer__binding__',
113 '__playwright_clock__',
114 '__pw_manual_fulfill__',
115 '__pw_dispatch_event__',
116 '__pwpEventListeners',
117 ];
118 _cdpArtifacts.forEach(function(key) {{
119 try {{ delete window[key]; }} catch(e) {{}}
120 try {{
121 if (key in window) {{
122 Object.defineProperty(window, key, {{
123 value: undefined, writable: false, configurable: false, enumerable: false
124 }});
125 }}
126 }} catch(e) {{}}
127 }});
128
129{stack_section}
130{console_section}
131
132 // ── 4. Navigator.prototype.webdriver — native-looking accessor descriptor ──
133 try {{
134 // Chrome's native descriptor: {{ get: f, set: undefined, enumerable: true, configurable: true }}
135 var _wdDesc = Object.getOwnPropertyDescriptor(Navigator.prototype, 'webdriver');
136 if (_wdDesc) {{
137 // If currently a data descriptor or has a set, redefine as accessor-only
138 if (!_wdDesc.get || _wdDesc.set !== undefined) {{
139 var _wdGetter = function webdriver() {{ return false; }};
140 _wdGetter.toString = function toString() {{
141 return 'function webdriver() {{ [native code] }}';
142 }};
143 Object.defineProperty(Navigator.prototype, 'webdriver', {{
144 get: _wdGetter,
145 set: undefined,
146 enumerable: true,
147 configurable: true,
148 }});
149 }} else {{
150 // Patch the existing getter to return false
151 var _existingGetter = _wdDesc.get;
152 // Only override if current getter would reveal webdriver=true
153 void _existingGetter; // referenced intentionally
154 var _falseGetter = function webdriver() {{ return false; }};
155 _falseGetter.toString = function toString() {{
156 return 'function webdriver() {{ [native code] }}';
157 }};
158 Object.defineProperty(Navigator.prototype, 'webdriver', {{
159 get: _falseGetter,
160 set: undefined,
161 enumerable: true,
162 configurable: true,
163 }});
164 }}
165 }}
166 }} catch(e) {{}}
167
168 // ── 5. Enumeration protection — mark CDP artifacts as non-enumerable ──
169 var _nonEnumProps = [
170 'cdc_adoQpoasnfa76pfcZLmcfl_Array',
171 'cdc_adoQpoasnfa76pfcZLmcfl_Promise',
172 'cdc_adoQpoasnfa76pfcZLmcfl_Symbol',
173 '__cdc_asdjflasutopfhvcZLmcfl_',
174 '__selenium_evaluate',
175 '__selenium_unwrapped',
176 '__webdriverFunc',
177 '__webdriver_evaluate',
178 '__driver_evaluate',
179 '__driver_unwrapped',
180 '__lastWatirAlert',
181 '__lastWatirConfirm',
182 '__lastWatirPrompt',
183 ];
184 _nonEnumProps.forEach(function(key) {{
185 try {{
186 if (key in window) {{
187 var _desc = Object.getOwnPropertyDescriptor(window, key);
188 if (_desc && _desc.enumerable) {{
189 Object.defineProperty(window, key, {{
190 value: _desc.value,
191 writable: _desc.writable || false,
192 configurable: _desc.configurable || false,
193 enumerable: false,
194 }});
195 }}
196 }}
197 }} catch(e) {{}}
198 }});
199
200}})();
201",
202 )
203}
204
205const ERROR_STACK_SECTION: &str = r" // ── 2. Sanitize Error.prototype.stack ───────────────────────────────
209 // Detects and removes frames from: __puppeteer, __playwright, pptr:, puppeteer-eval,
210 // playwright-eval, and chrome-extension:// URLs.
211 try {
212 var _origStackDesc = Object.getOwnPropertyDescriptor(Error.prototype, 'stack');
213 if (_origStackDesc && _origStackDesc.get) {
214 var _origStackGetter = _origStackDesc.get;
215 var _cdpFrameRe = /(https?:\/\/[^\s]*(?:__puppeteer|__playwright|pptr:|puppeteer-eval|playwright-eval)[^\s]*|chrome-extension:\/\/[^\s]*)/g;
216 var _sanitizedGetter = function stack() {
217 var s = _origStackGetter.call(this);
218 if (typeof s !== 'string') { return s; }
219 return s.replace(_cdpFrameRe, 'https://example.com/app.js');
220 };
221 _sanitizedGetter.toString = function toString() {
222 return 'function get stack() { [native code] }';
223 };
224 Object.defineProperty(Error.prototype, 'stack', {
225 get: _sanitizedGetter,
226 set: _origStackDesc.set,
227 enumerable: _origStackDesc.enumerable,
228 configurable: _origStackDesc.configurable,
229 });
230 } else if (_origStackDesc && 'value' in _origStackDesc) {
231 // Data-descriptor path: wrap with a getter going forward
232 // Nothing to patch here at definition time; stack is per-instance
233 }
234 } catch(e) {}";
235
236const CONSOLE_DEBUG_SECTION: &str = r" // ── 3. console.debug getter-trap hardening ────────────────────────────
238 try {
239 var _origDebug = console.debug.bind(console);
240 var _safeDebug = function debug() {
241 return _origDebug.apply(console, arguments);
242 };
243 _safeDebug.toString = function toString() {
244 return 'function debug() { [native code] }';
245 };
246 try {
247 console.debug = _safeDebug;
248 } catch(e) {
249 Object.defineProperty(console, 'debug', {
250 value: _safeDebug, writable: true, configurable: true, enumerable: false
251 });
252 }
253 } catch(e) {}";
254
255#[cfg(test)]
260mod tests {
261 use super::*;
262
263 fn default_script() -> String {
264 cdp_hardening_script(&CdpHardeningConfig::default())
265 }
266
267 #[test]
268 fn disabled_returns_empty() {
269 let js = cdp_hardening_script(&CdpHardeningConfig {
270 enabled: false,
271 ..Default::default()
272 });
273 assert!(js.is_empty());
274 }
275
276 #[test]
277 fn script_deletes_playwright_artifacts() {
278 let js = default_script();
279 assert!(
280 js.contains("__playwright__binding__"),
281 "missing playwright binding"
282 );
283 assert!(js.contains("__pwInitScripts"), "missing pwInitScripts");
284 assert!(
285 js.contains("__playwright_evaluation_script__"),
286 "missing playwright eval script"
287 );
288 assert!(
289 js.contains("__puppeteer_evaluation_script__"),
290 "missing puppeteer eval script"
291 );
292 }
293
294 #[test]
295 fn error_stack_sanitizer_regex_present() {
296 let js = default_script();
297 assert!(js.contains("__puppeteer"), "missing puppeteer pattern");
298 assert!(js.contains("__playwright"), "missing playwright pattern");
299 assert!(js.contains("pptr:"), "missing pptr: pattern");
300 assert!(
301 js.contains("chrome-extension://"),
302 "missing chrome-extension pattern"
303 );
304 }
305
306 #[test]
307 fn console_debug_has_native_tostring_spoof() {
308 let js = default_script();
309 assert!(
310 js.contains("function debug() { [native code] }"),
311 "missing native toString spoof for console.debug"
312 );
313 }
314
315 #[test]
316 fn webdriver_descriptor_matches_chrome_native() {
317 let js = default_script();
318 assert!(
320 js.contains("set: undefined"),
321 "missing set: undefined for webdriver"
322 );
323 assert!(
324 js.contains("enumerable: true"),
325 "webdriver must be enumerable"
326 );
327 assert!(
328 js.contains("configurable: true"),
329 "webdriver must be configurable"
330 );
331 }
332
333 #[test]
334 fn no_new_enumerable_window_properties() {
335 let js = default_script();
336 assert!(
338 js.contains("enumerable: false"),
339 "artifacts must be set non-enumerable"
340 );
341 }
342
343 #[test]
344 fn sanitize_stacks_false_omits_error_section() {
345 let js = cdp_hardening_script(&CdpHardeningConfig {
346 enabled: true,
347 sanitize_stacks: false,
348 protect_console: true,
349 });
350 assert!(
351 !js.contains("Error.prototype"),
352 "error section should be absent"
353 );
354 assert!(js.contains("console.debug"));
356 }
357
358 #[test]
359 fn protect_console_false_omits_console_section() {
360 let js = cdp_hardening_script(&CdpHardeningConfig {
361 enabled: true,
362 sanitize_stacks: true,
363 protect_console: false,
364 });
365 assert!(
367 !js.contains("_safeDebug"),
368 "console section should be absent"
369 );
370 assert!(js.contains("Error.prototype"));
372 }
373}