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.webdriverattribute 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:
| Signal | Weight | Patchable? |
|---|---|---|
| navigator.webdriver === true | High | Yes |
| navigator.webdriver patched (descriptor check) | High | Difficult |
| Playwright globals present | High | Yes |
| Chrome object incomplete | Medium | Yes |
| Missing plugins | Low | Yes |
| Permission API anomaly | Medium | Yes |
| Canvas fingerprint mismatch | Medium | Difficult |
| Timing entropy (low) | Low | Difficult |
| No mouse movement | Low | Yes |
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.