CDP Masking: Why Playwright and Puppeteer Get Detected in Seconds
Ready to protect your online identity?
Choose your plan and start running undetectable browser profiles today.
Chrome DevTools Protocol (CDP) is the standard interface for browser automation. Playwright, Puppeteer, and most Selenium-based setups use it to control Chromium instances. The problem is straightforward: every CDP connection leaves traces that anti-fraud systems detect within milliseconds of page load. No amount of stealth plugins, argument flags, or runtime patches fully eliminates these traces in Chromium — and the detection industry knows exactly where to look.
This article catalogs the complete set of CDP detection vectors as of 2026, explains why surface-level patches fail, and describes what kernel-level browser modification actually entails.
The Detection Surface: A Complete Inventory
CDP detection is not a single check. It is a layered system of dozens of signals, each individually weak but collectively damning. We can categorize them into five classes.
Class 1: The navigator.webdriver Flag
The most well-known detection vector. When Chromium is launched with automation enabled, navigator.webdriver returns true. The WebDriver specification (W3C) mandates this behavior.
// Detection check
if (navigator.webdriver === true) {
reportBot();
}
Common “fix”: Override via CDP:
await page.evaluateOnNewDocument(() => {
Object.defineProperty(navigator, 'webdriver', {
get: () => false,
});
});
Why it fails: This override executes in the page’s JavaScript context, but anti-detect scripts can probe for the override itself:
// Detecting the override
const descriptor = Object.getOwnPropertyDescriptor(navigator, 'webdriver');
if (descriptor && descriptor.get && descriptor.get.toString().includes('false')) {
reportBot(); // Property was overridden with a getter
}
// Checking the prototype chain
if (navigator.webdriver !== Navigator.prototype.webdriver) {
reportBot(); // Mismatch between instance and prototype
}
// Using iframe isolation
const iframe = document.createElement('iframe');
document.body.appendChild(iframe);
if (iframe.contentWindow.navigator.webdriver === true) {
reportBot(); // Override didn't propagate to new context
}
The iframe technique is particularly devastating: each new browsing context gets a fresh navigator object. If your override only patches the main frame, any dynamically created iframe will expose the real value.
Class 2: CDP-Specific Runtime Objects
When a CDP session is active, Chromium exposes internal objects that don’t exist in normal browser instances:
// Runtime.enable creates this
window.__cdp_runtime_enabled // internal flag
// Page.addScriptToEvaluateOnNewDocument artifacts
window.__playwright_evaluation_script_
// Puppeteer-specific
window.__puppeteer_utility_world__
Modern detection scripts enumerate all window properties and compare them against a known-clean browser profile:
function detectCDPArtifacts() {
const knownClean = new Set([/* hundreds of standard properties */]);
const current = Object.getOwnPropertyNames(window);
for (const prop of current) {
if (!knownClean.has(prop)) {
// Unknown property — potential automation artifact
logSuspicious(prop);
}
}
// Check for debugging protocols
if (window.cdc_adoQpoasnfa76pfcZLmcfl_Array ||
window.cdc_adoQpoasnfa76pfcZLmcfl_Promise ||
window.cdc_adoQpoasnfa76pfcZLmcfl_Symbol) {
reportBot(); // ChromeDriver control variables
}
}
The cdc_ prefix variables are ChromeDriver’s control variables, injected into every page. Their names are partially randomized per ChromeDriver build, but the pattern (cdc_ + random string + _ + JS type name) is consistent.
Class 3: Chrome Launch Arguments
Automation tools pass specific command-line arguments to Chromium. These arguments leave traces that JavaScript can detect:
# Typical Puppeteer launch args
--disable-blink-features=AutomationControlled
--enable-automation
--remote-debugging-port=9222
--disable-background-networking
--disable-default-apps
--disable-extensions
--disable-sync
--disable-translate
--metrics-recording-only
--no-first-run
Detection via Chrome internals:
// chrome.runtime is modified when --enable-automation is present
if (chrome.runtime && chrome.runtime.id === undefined &&
chrome.runtime.connect === undefined) {
reportBot(); // Automation mode strips runtime capabilities
}
// Check for missing permissions API behavior
if (!navigator.permissions ||
navigator.permissions.query === undefined) {
reportBot();
}
// Notification permission in automation mode
navigator.permissions.query({ name: 'notifications' }).then(result => {
if (result.state === 'denied' && !wasExplicitlyDenied()) {
reportBot(); // Automation mode auto-denies notifications
}
});
Class 4: Headless Mode Detection
Even with --headless=new (the “new headless” mode in Chrome 112+), differences persist:
// WebGL renderer string differences
const canvas = document.createElement('canvas');
const gl = canvas.getContext('webgl');
const renderer = gl.getParameter(gl.RENDERER);
const vendor = gl.getParameter(gl.VENDOR);
// Headless Chrome often returns:
// RENDERER: "SwiftShader" or "Google SwiftShader"
// VENDOR: "Google Inc. (Google)"
// Real Chrome returns actual GPU info:
// RENDERER: "ANGLE (NVIDIA, NVIDIA GeForce RTX 3080 Direct3D11 ...)"
// VENDOR: "Google Inc. (NVIDIA)"
// Plugin count
if (navigator.plugins.length === 0) {
reportBot(); // Headless has no plugins
}
// Language consistency
if (navigator.languages.length === 0 ||
navigator.languages[0] !== navigator.language) {
reportBot();
}
// Screen dimensions in headless
if (window.outerWidth === 0 || window.outerHeight === 0) {
reportBot();
}
Class 5: Behavioral Analysis
Beyond static property checks, anti-fraud systems analyze interaction patterns that CDP automation produces:
// Mouse movement patterns
// CDP's Input.dispatchMouseEvent creates synthetic events
// that lack the intermediate move events a real mouse produces
document.addEventListener('mousemove', (e) => {
movements.push({
x: e.clientX, y: e.clientY,
timestamp: e.timeStamp,
isTrusted: e.isTrusted
});
});
// After collecting events:
function analyzeMovements(events) {
// Real humans produce curved paths with acceleration
// CDP produces straight lines or no intermediate points
for (let i = 1; i < events.length; i++) {
const dt = events[i].timestamp - events[i-1].timestamp;
const dx = events[i].x - events[i-1].x;
const dy = events[i].y - events[i-1].y;
const distance = Math.sqrt(dx*dx + dy*dy);
// Teleporting mouse (large distance, small time)
if (distance > 100 && dt < 5) {
scores.teleport++;
}
// Perfectly straight lines (zero curvature)
if (i >= 2) {
const curvature = computeCurvature(
events[i-2], events[i-1], events[i]
);
if (curvature === 0) scores.straight++;
}
}
}
Why Stealth Plugins Are Insufficient
The puppeteer-extra-plugin-stealth package and similar solutions attempt to patch known detection vectors at runtime. Here is why this approach is architecturally flawed.
The Timing Problem
Stealth plugins inject patches via Page.addScriptToEvaluateOnNewDocument. This CDP command runs JavaScript before page scripts execute, but after the browser’s internal state is initialized. Anti-detect checks that run in the browser’s privileged context (e.g., Trusted Types, CSP violation handlers) can observe the original state before patches apply.
The Completeness Problem
Each Chromium update introduces new detection surfaces. The stealth plugin community is perpetually reactive — patching vectors after they appear in anti-fraud SDKs. The detection industry (DataDome, PerimeterX/HUMAN, Kasada, Akamai Bot Manager) employs full-time researchers discovering new vectors, while the open-source stealth community patches them weeks or months later.
The Consistency Problem
Patching individual properties creates inconsistencies. If you override navigator.webdriver but don’t patch the corresponding Blink internal that feeds it, cross-referencing the two reveals the lie:
// V8 internal check
const workerBlob = new Blob([`
postMessage(navigator.webdriver);
`], { type: 'application/javascript' });
const worker = new Worker(URL.createObjectURL(workerBlob));
worker.onmessage = (e) => {
if (e.data !== navigator.webdriver) {
reportBot(); // Worker has different webdriver value
}
};
Workers run in a separate context. If your patch only modifies the main window context, workers expose the true value.
Deep Kernel Patching: The Only Viable Approach
The term “kernel patching” in the anti-detect context refers to modifying the browser engine source code at the compilation level, not operating system kernel modifications. This is the approach taken by browsers like Camoufox (which Santiago is based on) and similar projects.
What Gets Patched
1. WebDriver Flag at the Engine Level
Instead of overriding navigator.webdriver via JavaScript, the flag is removed from the browser’s IDL definition:
// In Chromium: third_party/blink/renderer/core/frame/navigator.idl
// BEFORE:
[RuntimeEnabled=AutomationControlled] readonly attribute boolean webdriver;
// AFTER (patched):
// Line removed entirely — property doesn't exist at all
In Firefox/Gecko (Camoufox approach):
// dom/webidl/Navigator.webidl
// The webdriver attribute is compiled out of the build
// No runtime override needed — the property genuinely doesn't exist
2. CDP Artifact Removal
Control variables like cdc_* are eliminated from the driver source:
// Before: ChromeDriver injects control variables
ExecuteScript("window.cdc_" + randomSuffix + "_Array = Array;");
// After: Injection code removed entirely
// No variables to detect because they're never created
3. Automation Flags in the Binary
Chromium’s --enable-automation flag triggers numerous internal behaviors. In a patched build:
// content/browser/renderer_host/render_process_host_impl.cc
// AutomationControlled feature is disabled at compile time
// The runtime check for this flag returns false unconditionally
4. HeadlessMode Detection Surfaces
Patched browsers ensure headless and headed modes produce identical fingerprints:
// SwiftShader is replaced with real GPU emulation data
// Plugin list is populated identically in both modes
// Screen metrics report realistic values
Firefox/Gecko Advantages
Chromium is the most studied browser for automation detection — anti-fraud vendors invest heavily in Chromium-specific detection. Firefox receives less scrutiny, which is one reason Camoufox (and by extension Santiago) chose Gecko:
- Smaller detection surface: Fewer known Gecko automation artifacts exist in anti-fraud databases
- Different architecture: Gecko’s multi-process model differs from Chromium’s, so Chromium-specific detection scripts produce false positives on Firefox
- Marionette vs CDP: Firefox’s native automation protocol (Marionette) leaves different traces than CDP, and WebDriver BiDi (the successor) is even cleaner
- Fewer researchers targeting it: The economics of anti-fraud research favor Chromium analysis because most automation traffic uses Chromium
Detection Beyond the Browser
Even with perfect browser-level stealth, CDP-based automation can be detected through infrastructure signals:
Connection Patterns
Real user: DNS → TCP → TLS → HTTP (sequential, with natural delays)
CDP bot: Reuses connection pool, parallel requests, no idle time
Anti-fraud systems monitor connection-level behavior: how quickly requests follow page load, whether subresource requests follow a natural dependency order, and whether the connection exhibits idle periods consistent with human reading time.
WebSocket Debugging Port
CDP communicates over WebSocket. Even if the debugging port isn’t exposed externally, local port scanning from JavaScript can detect it:
// Port scanning via WebSocket timing
async function checkPort(port) {
const start = performance.now();
try {
const ws = new WebSocket(`ws://127.0.0.1:${port}`);
ws.onerror = () => {};
await new Promise(r => setTimeout(r, 100));
ws.close();
} catch (e) {}
const elapsed = performance.now() - start;
// Open port: fast connection or fast error
// Closed port: slower timeout
return elapsed < 50;
}
// Check common debugging ports
for (const port of [9222, 9229, 9333, 0]) {
if (await checkPort(port)) {
reportBot();
}
}
Memory and Performance Signatures
CDP operations leave traces in browser performance metrics:
// Automation inflates certain performance entries
const entries = performance.getEntriesByType('resource');
const cdpRequests = entries.filter(e =>
e.name.includes('devtools') ||
e.name.includes('inspector')
);
// Memory pressure from CDP overhead
if (performance.memory) {
const ratio = performance.memory.usedJSHeapSize /
performance.memory.totalJSHeapSize;
// CDP sessions increase baseline memory usage
if (ratio > 0.9 && performance.memory.jsHeapSizeLimit < 2e9) {
logSuspicious('memory_pressure');
}
}
The Santiago Approach: Protocol-Level Stealth
Santiago’s architecture avoids the CDP detection problem entirely through its choice of browser engine and automation protocol:
1. Gecko Engine: By using Firefox (via Camoufox), Santiago operates outside the primary CDP detection ecosystem. The vast majority of anti-fraud detection scripts are optimized for Chromium.
2. Source-Level Modifications: All automation indicators are removed at the source code level before compilation. There is no runtime patching — the automation artifacts simply don’t exist in the binary.
3. Native Profile Isolation: Each browser profile runs as an independent instance with its own state, cookies, local storage, and fingerprint. There is no shared CDP session multiplexing multiple profiles.
4. No Exposed Debugging Interface: The automation interface is not a standard debugging protocol but an internal IPC mechanism that doesn’t create network-visible endpoints.
Practical Verification: Testing Your Setup
If you need to verify whether your current automation setup is detectable, use these resources:
Online Detection Tests
- bot.sannysoft.com — comprehensive automation detection test
- browserleaks.com — full fingerprint analysis including CDP artifacts
- abrahamjuliot.github.io/creepjs — CreepJS, one of the most aggressive fingerprinting tools
Manual Verification Script
// Run this in your automated browser's console
const checks = {
webdriver: navigator.webdriver,
webdriverProto: Navigator.prototype.webdriver,
plugins: navigator.plugins.length,
languages: navigator.languages.length,
chrome: !!window.chrome,
permissions: !!navigator.permissions,
webgl: (() => {
const c = document.createElement('canvas');
const gl = c.getContext('webgl');
return gl ? gl.getParameter(gl.RENDERER) : 'unavailable';
})(),
cdcVars: Object.getOwnPropertyNames(window)
.filter(p => p.startsWith('cdc_')),
domAutomation: !!window.domAutomation,
domAutomationController: !!window.domAutomationController,
};
console.table(checks);
Conclusion
CDP-based automation detection is not a cat-and-mouse game with a balanced playing field. The detection side has structural advantages: they control the JavaScript execution environment, they can update detection scripts server-side in seconds, and they benefit from shared intelligence across thousands of sites using the same anti-fraud SDK.
The only sustainable defense is to not be detectable in the first place. This means either using a browser engine with automation traces removed at the source code level, or accepting that your CDP-based Playwright/Puppeteer setup will be flagged on any site running modern anti-fraud protection. There is no middle ground — stealth plugins are a temporary patch over a permanent architectural flaw.
Ready to protect your online identity?
Choose your plan and start running undetectable browser profiles today.
Earn 15% lifetime commission on every referral.
Become a Partner →