Technical Deep-Dive

navigator.webdriver: The Definitive Guide to Headless Browser Detection

navigator.webdriver is the most well-known bot detection signal — but modern automation frameworks patch it. Here is the complete picture.

10 min readJanuary 15, 2025By Shlumi Team

What is navigator.webdriver?

The navigator.webdriver property is a boolean flag defined in the W3C WebDriver specification. When a browser is controlled by an automation framework (Selenium, Playwright, Puppeteer, WebDriverIO, etc.), this property is set to true. In a real browser session, it is undefined or false.

The intent of this flag is to allow web applications to detect when they are being tested by automated tools — a legitimate use case for test environments. However, it has also become the most widely used bot detection signal, and consequently, the most widely patched by automation frameworks trying to evade detection.

The WebDriver Specification

According to the W3C WebDriver specification (section 8.1), when a browser is in "webdriver mode":

"The navigator.webdriver attribute MUST return true if the current browsing context's active document's origin is a WebDriver session."

This means that any standards-compliant browser automation framework is required to set this flag. Selenium, WebDriverIO, and other standards-based tools comply with this requirement.

How Automation Frameworks Patch navigator.webdriver

Modern stealth-focused automation frameworks patch navigator.webdriver to evade detection. The most common approaches:

Object.defineProperty Override

// Playwright's stealth plugin patches navigator.webdriver
Object.defineProperty(navigator, 'webdriver', {
  get: () => undefined,
  configurable: true
});

CDP Runtime.evaluate Injection

Frameworks can inject JavaScript before any page scripts run using CDP's Page.addScriptToEvaluateOnNewDocument command. This allows them to patch navigator.webdriver before your detection code runs.

Chromium Build Patches

Some automation frameworks use custom Chromium builds with the navigator.webdriver flag removed at the C++ level. These are essentially undetectable via this property alone.

Detecting the Patch Itself

Interestingly, the act of patching navigator.webdriver is itself detectable. When a property is overridden with Object.defineProperty, the property descriptor changes in ways that reveal the modification:

// Check if navigator.webdriver has been tampered with
const descriptor = Object.getOwnPropertyDescriptor(navigator, 'webdriver');
if (descriptor) {
  // Property has been overridden — suspicious
  // In a real browser, navigator.webdriver is on the prototype, not the instance
  const isPatched = descriptor.get?.toString().includes('undefined') ||
                    descriptor.configurable === true;
}

In a real Chrome browser, navigator.webdriver is defined on the Navigator prototype, not on the navigator instance. If Object.getOwnPropertyDescriptor(navigator, 'webdriver') returns a descriptor (rather than undefined), the property has been patched.

What to Check When navigator.webdriver is Patched

Since navigator.webdriver alone is insufficient, a comprehensive detection strategy must include additional signals:

1. Chrome Object Completeness

const checks = {
  hasChrome: typeof window.chrome !== 'undefined',
  hasRuntime: typeof window.chrome?.runtime !== 'undefined',
  hasLoadTimes: typeof window.chrome?.loadTimes === 'function',
  hasCsi: typeof window.chrome?.csi === 'function',
};

2. Plugin and MimeType Enumeration

// Real Chrome has plugins; headless Chrome often does not
const pluginCount = navigator.plugins.length;
const mimeTypeCount = navigator.mimeTypes.length;
// Both should be > 0 in a real browser

3. Permissions API Consistency

const notifPerm = await navigator.permissions.query({ name: 'notifications' });
// In a fresh real browser: 'default' or 'prompt'
// In many automation frameworks: 'denied'

4. Error Stack Analysis

function getStackTrace() {
  try { throw new Error(); } catch(e) { return e.stack; }
}
const stack = getStackTrace();
// CDP-injected scripts appear in stack traces with unusual frame patterns

5. Timing Entropy

// Measure time between consecutive Date.now() calls
// Automation frameworks often have lower timing entropy than real browsers
const samples = Array.from({ length: 100 }, () => performance.now());
const deltas = samples.slice(1).map((v, i) => v - samples[i]);
const entropy = Math.std(deltas); // Low entropy = suspicious

The Complete Detection Matrix

A production-ready headless browser detection system should check all of the following:

SignalWeightPatchable?
navigator.webdriver === trueHighYes
navigator.webdriver patched (descriptor check)HighDifficult
Playwright globals presentHighYes
Chrome object incompleteMediumYes
Missing pluginsLowYes
Permission API anomalyMediumYes
Canvas fingerprint mismatchMediumDifficult
Timing entropy (low)LowDifficult
No mouse movementLowYes

Conclusion

navigator.webdriver is a useful starting point for headless browser detection, but it cannot be relied upon alone. Modern automation frameworks patch it trivially. A robust detection system must combine multiple signals — including the detection of patching attempts themselves — and use confidence scoring to make accurate verdicts. Shlumi's detection engine implements all of these checks automatically, giving you a comprehensive view of browser automation activity on your website.

Topics

navigator.webdriverheadless browser detectionwebdriver detectiondetect headless Chromebrowser automation detectionCDP detection

Protect your site from AI agents

Shlumi detects Claude, Gemini, Playwright, Puppeteer, and 30+ other automation frameworks with a single script tag. Free tier includes 1,000 sessions/month.

Get started free

Related articles

Detection Techniques

How to Detect Claude Browser Automation on Your Website

8 min read

Detection Techniques

Playwright Detection Techniques: How to Identify Automated Browser Testing

6 min read