CDP Masking: Why Playwright and Puppeteer Get Detected in Seconds

· 15 min read
cdp playwright puppeteer detection automation
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.

Get Started

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:

  1. Smaller detection surface: Fewer known Gecko automation artifacts exist in anti-fraud databases
  2. Different architecture: Gecko’s multi-process model differs from Chromium’s, so Chromium-specific detection scripts produce false positives on Firefox
  3. Marionette vs CDP: Firefox’s native automation protocol (Marionette) leaves different traces than CDP, and WebDriver BiDi (the successor) is even cleaner
  4. 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 →