AlterLabAlterLab
Web Scraping

Selenium Bot Detection: Why You Get Caught and How to Avoid It

Selenium leaks over a dozen browser signals that anti-bot systems detect instantly. This guide covers every detection vector from navigator.webdriver to CDP markers, with working Python fixes for each one.

Yash Dubey

Yash Dubey

February 19, 2026

10 min read
72 views
Share:

Every time you launch a Selenium script against a production website, you are walking into a room full of tripwires. Modern anti-bot systems do not look for one signal. They look for dozens, and Selenium hands them over willingly.

This guide covers every way Selenium leaks its identity, the specific fix for each leak, and how to build a stealth configuration that survives real-world detection. Code is Python 3.11+ with Selenium 4.

Why Selenium Gets Detected

Anti-bot systems like Cloudflare, DataDome, PerimeterX, and Akamai Bot Manager run JavaScript fingerprinting on every page load. They inspect your browser environment for inconsistencies that no real user would have. Selenium introduces many of these inconsistencies by default.

The detection is not a single check. It is a scoring system. Each leaked signal adds points. Cross a threshold, and you get a CAPTCHA, a 403, or an infinite redirect loop.

12+Browser Properties Selenium Leaks
~80%Sites Using Bot Detection in 2025
<100msTime to Fingerprint a Browser
0Warnings Before You Get Blocked

The Detection Signals: Everything Selenium Leaks

1. The navigator.webdriver Flag

This is the most well-known detection vector. When ChromeDriver controls a browser, it sets navigator.webdriver to true. Every real browser returns false or undefined.

python
from selenium import webdriver

driver = webdriver.Chrome()
driver.get("https://example.com")

# This returns True in Selenium-controlled browsers
result = driver.execute_script("return navigator.webdriver")
print(result)  # True -- you are caught

Anti-bot scripts check this property before the page even finishes loading. It is usually the first thing they test.

The fix:

python
from selenium import webdriver
from selenium.webdriver.chrome.options import Options

options = Options()
options.add_argument("--disable-blink-features=AutomationControlled")

driver = webdriver.Chrome(options=options)

# Override the property via CDP
driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
    "source": "Object.defineProperty(navigator, 'webdriver', { get: () => undefined });"
})

The --disable-blink-features=AutomationControlled flag prevents Chrome from setting the flag in the first place. The CDP override catches any edge cases where it leaks through iframes or service workers.

2. ChromeDriver cdc_ Marker Variables

ChromeDriver injects variables into the document object that start with cdc_ (e.g., cdc_adoQpoasnfa76pfcZLmcfl_Array). These are internal communication channels between ChromeDriver and the browser. No regular Chrome session has them.

Detection code looks like this on the anti-bot side:

javascript
// What anti-bot scripts run against your browser
let dominated = false;
for (let key in document) {
    if (key.match(/^cdc_/)) {
        dominated = true;
        break;
    }
}

The fix:

The cleanest approach is to patch the ChromeDriver binary. This is fragile and version-specific, so most engineers use undetected-chromedriver instead, which handles this automatically:

python
import undetected_chromedriver as uc

driver = uc.Chrome(version_main=124)
driver.get("https://nowsecure.nl")

undetected-chromedriver patches the ChromeDriver binary at runtime, renaming cdc_ variables to random strings. It also handles several other detection vectors automatically.

3. Missing Browser Plugins

A real Chrome browser reports plugins like Chrome PDF Viewer, Native Client, and Widevine Content Decryption Module. Headless Chrome reports an empty plugin array.

python
# Detection check
plugins = driver.execute_script("return navigator.plugins.length")
# Real Chrome: 3-5 plugins
# Headless Chrome: 0 plugins

The fix:

python
driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
    "source": """
        Object.defineProperty(navigator, 'plugins', {
            get: () => [
                { name: 'Chrome PDF Plugin', filename: 'internal-pdf-viewer' },
                { name: 'Chrome PDF Viewer', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai' },
                { name: 'Native Client', filename: 'internal-nacl-plugin' }
            ]
        });
    """
})

4. User-Agent String Mismatches

The default Selenium user-agent contains HeadlessChrome when running in headless mode. Even in headed mode, the version string can be inconsistent with the actual Chrome binary.

python
# Headless default UA contains "HeadlessChrome"
ua = driver.execute_script("return navigator.userAgent")
# Mozilla/5.0 ... HeadlessChrome/124.0.6367.91 ...

The fix:

python
options = Options()
options.add_argument("--headless=new")
options.add_argument(
    "--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
    "AppleWebKit/537.36 (KHTML, like Gecko) "
    "Chrome/124.0.6367.91 Safari/537.36"
)

But the user-agent is only one piece. Anti-bot systems cross-reference it with other signals. If your UA says Windows but navigator.platform says Linux, you are flagged.

5. The Headless vs Headed Detection Matrix

Running --headless changes more than just the user-agent. Anti-bot scripts probe multiple properties to distinguish headless from headed mode.

FeaturePropertyHeaded ChromeHeadless Chrome
navigator.webdriverfalsetrue
navigator.plugins.length3-50
navigator.languagesen-US and enempty string
window.chromeobjectundefined
Notification.permissiondefaultdenied (always)
navigator.userAgentChrome/XXXHeadlessChrome/XXX
WebGL rendererGPU nameSwiftShader
window.outerWidthgreater than 00

Every one of these mismatches is a detection signal. Fixing only one or two still leaves you exposed.

6. WebGL and Canvas Fingerprinting

Headless Chrome uses SwiftShader for WebGL rendering. Anti-bot scripts query UNMASKED_VENDOR_WEBGL and UNMASKED_RENDERER_WEBGL to check for this.

python
# What detection scripts extract
renderer = driver.execute_script("""
    const canvas = document.createElement('canvas');
    const gl = canvas.getContext('webgl');
    const ext = gl.getExtension('WEBGL_debug_renderer_info');
    return gl.getParameter(ext.UNMASKED_RENDERER_WEBGL);
""")
# Real Chrome: "ANGLE (NVIDIA GeForce GTX 1080 ...)"
# Headless: "Google SwiftShader"

The fix:

There is no clean JavaScript override for this. The most reliable approach is to use headed mode with a virtual display (Xvfb on Linux):

python
# On Linux, use xvfb for a virtual display
# pip install PyVirtualDisplay
from pyvirtualdisplay import Display

display = Display(visible=0, size=(1920, 1080))
display.start()

options = Options()
# Do NOT use --headless. Use the virtual display instead.
driver = webdriver.Chrome(options=options)

This gives you a real GPU-accelerated rendering context without needing a physical monitor.

7. HTTP Header Anomalies

Selenium sends HTTP headers in a different order than real browsers, and it often omits headers like sec-ch-ua, sec-ch-ua-mobile, and sec-fetch-dest that modern Chrome always sends.

FeatureHeaderReal ChromeSelenium Default
sec-ch-uaPresent with brand hintsMissing
sec-ch-ua-platformWindows or macOSMissing
sec-fetch-sitenone or same-originMissing
Accept-Languageen-US with quality valuesBare en-US and en
Header orderConsistent per versionVaries by driver version

The fix using CDP Network.setExtraHTTPHeaders:

python
driver.execute_cdp_cmd("Network.setExtraHTTPHeaders", {
    "headers": {
        "sec-ch-ua": '"Chromium";v="124", "Google Chrome";v="124", "Not-A.Brand";v="99"',
        "sec-ch-ua-mobile": "?0",
        "sec-ch-ua-platform": '"Windows"',
        "Accept-Language": "en-US,en;q=0.9",
    }
})

Chrome exposes several flags via chrome://flags and the DevTools protocol that reveal automation. The enable-automation flag, in particular, adds an info bar that says "Chrome is being controlled by automated test software" and sets detectable properties.

python
options = Options()
options.add_experimental_option("excludeSwitches", ["enable-automation"])
options.add_experimental_option("useAutomationExtension", False)

9. JavaScript Environment Leaks

Beyond navigator.webdriver, there are subtler JavaScript environment differences:

python
driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
    "source": """
        // Fix window.chrome
        window.chrome = {
            runtime: {},
            loadTimes: function() {},
            csi: function() {},
            app: { isInstalled: false }
        };

        // Fix permissions API
        const originalQuery = window.navigator.permissions.query;
        window.navigator.permissions.query = (parameters) => (
            parameters.name === 'notifications' ?
            Promise.resolve({ state: Notification.permission }) :
            originalQuery(parameters)
        );

        // Fix languages
        Object.defineProperty(navigator, 'languages', {
            get: () => ['en-US', 'en']
        });
    """
})

The Complete Stealth Setup

Here is a full stealth configuration that addresses every detection vector covered above.

1

Install Dependencies

pip install selenium undetected-chromedriver selenium-stealth PyVirtualDisplay

2

Configure Browser Options

Disable automation flags, set realistic user-agent, configure window size

3

Inject CDP Overrides

Patch navigator properties, plugins, permissions, and Chrome object before page load

4

Set Network Headers

Add sec-ch-ua headers, fix Accept-Language, match header order to real Chrome

5

Add Human Behavior

Random delays, mouse movements, scroll patterns, and viewport resizing

Option A: Using selenium-stealth

The selenium-stealth library automates many of the patches described above:

python
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium_stealth import stealth

options = Options()
options.add_argument("--headless=new")
options.add_argument("--window-size=1920,1080")
options.add_argument("--disable-blink-features=AutomationControlled")
options.add_experimental_option("excludeSwitches", ["enable-automation"])
options.add_experimental_option("useAutomationExtension", False)
options.add_argument(
    "--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
    "AppleWebKit/537.36 (KHTML, like Gecko) "
    "Chrome/124.0.6367.91 Safari/537.36"
)

driver = webdriver.Chrome(options=options)

stealth(driver,
    languages=["en-US", "en"],
    vendor="Google Inc.",
    platform="Win32",
    webgl_vendor="Intel Inc.",
    renderer="Intel Iris OpenGL Engine",
    fix_hairline=True,
)

driver.get("https://bot.sannysoft.com")

Option B: Using undetected-chromedriver

For sites with aggressive detection (Cloudflare, DataDome), undetected-chromedriver is more effective because it patches the ChromeDriver binary itself:

python
import undetected_chromedriver as uc
import time

options = uc.ChromeOptions()
options.add_argument("--window-size=1920,1080")
options.add_argument(
    "--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
    "AppleWebKit/537.36 (KHTML, like Gecko) "
    "Chrome/124.0.6367.91 Safari/537.36"
)

driver = uc.Chrome(options=options, version_main=124)

# Add extra CDP patches for thorough coverage
driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
    "source": """
        Object.defineProperty(navigator, 'languages', {
            get: () => ['en-US', 'en']
        });
        Object.defineProperty(navigator, 'plugins', {
            get: () => [
                { name: 'Chrome PDF Plugin' },
                { name: 'Chrome PDF Viewer' },
                { name: 'Native Client' }
            ]
        });
    """
})

driver.get("https://nowsecure.nl")
time.sleep(5)

# Check if we passed
title = driver.title
print(f"Page title: {title}")
driver.quit()

Option C: Headless Firefox as an Alternative

Firefox with geckodriver has fewer default leaks than Chrome. The navigator.webdriver flag is the primary concern, and it can be disabled via preferences:

python
from selenium import webdriver
from selenium.webdriver.firefox.options import Options

options = Options()
options.add_argument("--headless")
options.set_preference("dom.webdriver.enabled", False)
options.set_preference("useAutomationExtension", False)
options.set_preference(
    "general.useragent.override",
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:125.0) "
    "Gecko/20100101 Firefox/125.0"
)

driver = webdriver.Firefox(options=options)
driver.get("https://bot.sannysoft.com")

Firefox lacks the cdc_ variable problem and the window.chrome detection vector entirely, making it a cleaner starting point for stealth scraping. However, some sites specifically check for Chrome-like features and will flag Firefox-based access as suspicious if combined with other anomalies.

Behavioral Detection: The Layer You Cannot Patch

Even with a perfectly patched browser fingerprint, modern anti-bot systems use behavioral analysis. They measure:

  • Mouse movement patterns: Real users move the mouse in curved, imprecise paths. Bot scripts move in straight lines or not at all.
  • Scroll behavior: Real users scroll in variable increments with momentum. Bots jump to elements instantly.
  • Timing patterns: Real users have variable delays between actions. Bots operate at consistent intervals.
  • Viewport interaction: Real users resize windows, switch tabs, and trigger focus/blur events.
python
import random
import time
from selenium.webdriver.common.action_chains import ActionChains

def human_like_delay(min_sec=0.5, max_sec=2.0):
    time.sleep(random.uniform(min_sec, max_sec))

def human_scroll(driver):
    total_height = driver.execute_script("return document.body.scrollHeight")
    current = 0
    while current < total_height:
        scroll_amount = random.randint(200, 600)
        driver.execute_script(f"window.scrollBy(0, {scroll_amount});")
        current += scroll_amount
        human_like_delay(0.3, 1.2)

def human_mouse_move(driver, element):
    actions = ActionChains(driver)
    x_offset = random.randint(-5, 5)
    y_offset = random.randint(-5, 5)
    actions.move_to_element_with_offset(element, x_offset, y_offset)
    actions.perform()
    human_like_delay(0.1, 0.4)

Detection Test: How to Verify Your Setup

Before running your scraper against a target, test it against known detection pages:

Detection Test Sites by Coverage
python
# Quick detection test
import undetected_chromedriver as uc

driver = uc.Chrome(version_main=124)
driver.get("https://bot.sannysoft.com")

# Screenshot the results
driver.save_screenshot("/tmp/detection_test.png")

# Check specific values
webdriver_flag = driver.execute_script("return navigator.webdriver")
chrome_obj = driver.execute_script("return typeof window.chrome")
plugins_count = driver.execute_script("return navigator.plugins.length")

print(f"webdriver: {webdriver_flag}")   # Should be: undefined/False
print(f"chrome: {chrome_obj}")          # Should be: object
print(f"plugins: {plugins_count}")      # Should be: 3+

driver.quit()

When Stealth Selenium Is Not Enough

There is a hard ceiling to what browser patching can achieve. Anti-bot vendors update their detection methods continuously. A setup that works today may fail next week when Cloudflare ships a new fingerprinting check.

The fundamental problem: you are playing defense against a system that knows every possible automation tool. Every patch is a reaction to a detection that already exists.

At scale, maintaining stealth Selenium becomes an ongoing engineering burden:

  • ChromeDriver updates break existing patches
  • New fingerprinting techniques require research and counter-patches
  • IP reputation means proxies must be rotated and managed
  • CAPTCHA solving adds cost and latency
  • Memory and CPU costs scale linearly with concurrent sessions

For production scraping workloads, a dedicated scraping API handles the anti-bot bypass at the infrastructure level. AlterLab, for example, maintains rotating browser fingerprints, residential proxies, and anti-bot bypass logic as a managed service -- you send a URL and get back clean HTML without managing any browser infrastructure.

python
import requests

# Instead of managing Selenium stealth patches:
response = requests.post(
    "https://alterlab.io/api/v1/scrape",
    headers={"X-API-Key": "your_api_key"},
    json={"url": "https://example.com", "formats": ["html", "markdown"]}
)

data = response.json()
html_content = data["results"]["html"]

This is not about choosing one approach over the other. Stealth Selenium is the right tool for low-volume scraping, testing, and development. A scraping API is the right tool for production workloads where reliability and maintenance cost matter more than per-request cost.

Summary: Detection Vectors and Fixes

FeatureDetection VectorRisk LevelFix
navigator.webdriverCriticalCDP override + Chrome flag
cdc_ variablesCriticalundetected-chromedriver
Empty plugins arrayHighJavaScript property override
HeadlessChrome UAHighCustom user-agent string
Missing sec-ch-ua headersMediumCDP Network.setExtraHTTPHeaders
SwiftShader WebGLMediumXvfb virtual display (headed mode)
window.chrome missingMediumJavaScript property injection
Behavioral patternsHighRandomized human-like interactions

The detection game is asymmetric. Defenders only need to find one signal you missed. Attackers need to patch every signal perfectly, across every page load, across every Chrome version update. Understanding the full scope of what Selenium leaks is the first step toward either patching it effectively or deciding to use a tool that handles it for you.

Yash Dubey

Yash Dubey