A Complete Guide to Masking Selenium and Puppeteer from Detection
Modern anti-bot protection systems easily recognize automated browsers by dozens of signs: from JavaScript variables to WebDriver behavior characteristics. Websites use Cloudflare, DataDome, PerimeterX, and their own solutions that block up to 90% of requests from Selenium and Puppeteer in standard configurations.
In this guide, we will cover all automation masking methods: from basic settings to advanced detection bypass techniques. You will receive ready-made solutions with code examples for Python and Node.js that work against most protective systems.
How Websites Detect Automation
Anti-bot protection systems analyze the browser based on multiple parameters simultaneously. Even if you hide one sign, the others will reveal automation. Understanding all detection methods is key to effective masking.
WebDriver Indicators
The simplest detection method is checking JavaScript variables that are present only in automated browsers:
// These variables reveal Selenium/Puppeteer
navigator.webdriver === true
window.navigator.webdriver === true
document.$cdc_ // ChromeDriver specific variable
window.document.documentElement.getAttribute("webdriver")
navigator.plugins.length === 0 // Automated browsers have no plugins
navigator.languages === "" // Empty language list
Cloudflare and similar systems check these properties first. If any of them return a positive result, the request is blocked.
Browser Fingerprinting
Advanced systems create a unique fingerprint of the browser based on dozens of parameters:
- Canvas Fingerprinting — rendering hidden images and analyzing pixel data
- WebGL Fingerprinting — parameters of the graphics renderer and video card
- Audio Context — unique characteristics of audio processing
- Fonts Fingerprinting — list of installed fonts in the system
- Screen Resolution — screen resolution, color depth, available area
- Timezone & Language — timezone, browser languages, system locale
Automated browsers often have atypical combinations of these parameters. For example, Headless Chrome has no plugins but supports WebGL — such a combination is extremely rare among real users.
Behavioral Analysis
Modern systems track behavioral patterns:
- Mouse movements — bots move the cursor in straight lines or do not move it at all
- Speed of actions — instant form filling, inhuman click speed
- Scrolling patterns — sharp jumps instead of smooth scrolling
- Keyboard events — absence of natural delays between keystrokes
- Request frequency — overly regular intervals between actions
Important: DataDome and PerimeterX use machine learning to analyze behavior. They are trained on millions of sessions and can recognize a bot even with proper masking of technical parameters if the behavior appears unnatural.
Basic Selenium Masking
Selenium WebDriver in its standard configuration leaves many traces. Let's consider a step-by-step setup to minimize detection using Python and ChromeDriver.
Disabling the WebDriver Flag
The first step is to hide the navigator.webdriver variable. This is done through the Chrome DevTools Protocol:
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options
# Setting up Chrome options
chrome_options = Options()
# Disabling the automation flag
chrome_options.add_experimental_option("excludeSwitches", ["enable-automation"])
chrome_options.add_experimental_option('useAutomationExtension', False)
# Creating the driver
driver = webdriver.Chrome(options=chrome_options)
# Removing webdriver via CDP
driver.execute_cdp_cmd('Page.addScriptToEvaluateOnNewDocument', {
'source': '''
Object.defineProperty(navigator, 'webdriver', {
get: () => undefined
})
'''
})
driver.get('https://example.com')
Configuring User-Agent and Other Headers
Headless browsers often use outdated or specific User-Agent strings. It is necessary to set a current User-Agent of a real browser:
# Current User-Agent for Chrome on Windows
user_agent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
chrome_options.add_argument(f'user-agent={user_agent}')
# Additional arguments for masking
chrome_options.add_argument('--disable-blink-features=AutomationControlled')
chrome_options.add_argument('--disable-dev-shm-usage')
chrome_options.add_argument('--no-sandbox')
chrome_options.add_argument('--disable-gpu')
# Window size as a real user
chrome_options.add_argument('--window-size=1920,1080')
chrome_options.add_argument('--start-maximized')
Adding Plugins and Languages
Automated browsers have no plugins and often show an empty language list. We fix this via CDP:
driver.execute_cdp_cmd('Page.addScriptToEvaluateOnNewDocument', {
'source': '''
Object.defineProperty(navigator, 'languages', {
get: () => ['en-US', 'en', 'ru']
});
Object.defineProperty(navigator, 'plugins', {
get: () => [
{
0: {type: "application/x-google-chrome-pdf", suffixes: "pdf", description: "Portable Document Format"},
description: "Portable Document Format",
filename: "internal-pdf-viewer",
length: 1,
name: "Chrome PDF Plugin"
},
{
0: {type: "application/pdf", suffixes: "pdf", description: ""},
description: "",
filename: "mhjfbmdgcfjbbpaeojofohoefgiehjai",
length: 1,
name: "Chrome PDF Viewer"
}
]
});
Object.defineProperty(navigator, 'platform', {
get: () => 'Win32'
});
'''
})
Complete Example of Selenium Setup
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
import random
def create_stealth_driver():
chrome_options = Options()
# Basic masking settings
chrome_options.add_experimental_option("excludeSwitches", ["enable-automation"])
chrome_options.add_experimental_option('useAutomationExtension', False)
chrome_options.add_argument('--disable-blink-features=AutomationControlled')
# User-Agent
user_agent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
chrome_options.add_argument(f'user-agent={user_agent}')
# Window size
chrome_options.add_argument('--window-size=1920,1080')
chrome_options.add_argument('--start-maximized')
driver = webdriver.Chrome(options=chrome_options)
# Masking script via CDP
stealth_script = '''
Object.defineProperty(navigator, 'webdriver', {get: () => undefined});
Object.defineProperty(navigator, 'languages', {get: () => ['en-US', 'en']});
Object.defineProperty(navigator, 'platform', {get: () => 'Win32'});
window.chrome = {
runtime: {}
};
Object.defineProperty(navigator, 'plugins', {
get: () => [1, 2, 3, 4, 5]
});
'''
driver.execute_cdp_cmd('Page.addScriptToEvaluateOnNewDocument', {
'source': stealth_script
})
return driver
# Usage
driver = create_stealth_driver()
driver.get('https://bot.sannysoft.com/') # Site for detection testing
Configuring Puppeteer to Bypass Detection
Puppeteer has the same detection issues as Selenium. However, there is a ready-made library for Node.js called puppeteer-extra-plugin-stealth that automates most masking settings.
Installing puppeteer-extra
npm install puppeteer puppeteer-extra puppeteer-extra-plugin-stealth
Basic Configuration with Stealth Plugin
const puppeteer = require('puppeteer-extra');
const StealthPlugin = require('puppeteer-extra-plugin-stealth');
// Using the stealth plugin
puppeteer.use(StealthPlugin());
(async () => {
const browser = await puppeteer.launch({
headless: 'new', // New headless mode for Chrome
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-accelerated-2d-canvas',
'--disable-gpu',
'--window-size=1920,1080',
'--disable-web-security',
'--disable-features=IsolateOrigins,site-per-process'
]
});
const page = await browser.newPage();
// Setting the viewport
await page.setViewport({
width: 1920,
height: 1080,
deviceScaleFactor: 1
});
// Setting User-Agent
await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36');
// Additional headers
await page.setExtraHTTPHeaders({
'Accept-Language': 'en-US,en;q=0.9',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8'
});
await page.goto('https://bot.sannysoft.com/');
// Screenshot for testing
await page.screenshot({ path: 'test.png' });
await browser.close();
})();
Manual Setup Without Plugins
If you want full control or cannot use third-party libraries, set up masking manually:
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch({
headless: 'new',
args: ['--no-sandbox', '--disable-setuid-sandbox']
});
const page = await browser.newPage();
// Overriding webdriver and other properties
await page.evaluateOnNewDocument(() => {
Object.defineProperty(navigator, 'webdriver', {
get: () => undefined
});
Object.defineProperty(navigator, 'languages', {
get: () => ['en-US', 'en']
});
Object.defineProperty(navigator, 'plugins', {
get: () => [1, 2, 3, 4, 5]
});
// Masking Chrome headless
Object.defineProperty(navigator, 'platform', {
get: () => 'Win32'
});
window.chrome = {
runtime: {}
};
// Overriding permissions
const originalQuery = window.navigator.permissions.query;
window.navigator.permissions.query = (parameters) => (
parameters.name === 'notifications' ?
Promise.resolve({ state: Notification.permission }) :
originalQuery(parameters)
);
});
await page.goto('https://example.com');
await browser.close();
})();
Configuration to Bypass Cloudflare
Cloudflare uses advanced detection methods. To bypass it, you need to add random delays and emulate actions:
const puppeteer = require('puppeteer-extra');
const StealthPlugin = require('puppeteer-extra-plugin-stealth');
puppeteer.use(StealthPlugin());
async function bypassCloudflare(url) {
const browser = await puppeteer.launch({
headless: 'new',
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-web-security'
]
});
const page = await browser.newPage();
// Random User-Agent
const userAgents = [
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
];
await page.setUserAgent(userAgents[Math.floor(Math.random() * userAgents.length)]);
// Navigating to the page
await page.goto(url, { waitUntil: 'networkidle2' });
// Waiting for Cloudflare check (usually 5-10 seconds)
await page.waitForTimeout(8000);
// Random mouse movement
await page.mouse.move(100, 100);
await page.mouse.move(200, 200);
const content = await page.content();
await browser.close();
return content;
}
Combating JavaScript Fingerprinting
JavaScript fingerprinting is the creation of a unique browser fingerprint based on numerous parameters. Even if you hide the webdriver, systems analyze hundreds of other properties to detect automation.
Main Fingerprinting Vectors
Anti-bot protection systems check the following parameters:
| Parameter | What is Checked | Detection Risk |
|---|---|---|
| navigator.webdriver | Presence of the automation flag | Critical |
| navigator.plugins | Number and types of plugins | High |
| window.chrome | Presence of Chrome API | Medium |
| navigator.permissions | Browser permissions API | Medium |
| screen.colorDepth | Screen color depth | Low |
| navigator.hardwareConcurrency | Number of CPU cores | Low |
Comprehensive Masking Script
The script below overrides most problematic properties. Implement it via CDP (Selenium) or evaluateOnNewDocument (Puppeteer):
const stealthScript = `
// Removing webdriver
Object.defineProperty(navigator, 'webdriver', {
get: () => undefined
});
// Adding chrome object
window.chrome = {
runtime: {},
loadTimes: function() {},
csi: function() {},
app: {}
};
// Overriding permissions
const originalQuery = window.navigator.permissions.query;
window.navigator.permissions.query = (parameters) => (
parameters.name === 'notifications' ?
Promise.resolve({ state: Notification.permission }) :
originalQuery(parameters)
);
// Masking plugins
Object.defineProperty(navigator, 'plugins', {
get: () => {
return [
{
0: {type: "application/x-google-chrome-pdf", suffixes: "pdf"},
description: "Portable Document Format",
filename: "internal-pdf-viewer",
length: 1,
name: "Chrome PDF Plugin"
},
{
0: {type: "application/pdf", suffixes: "pdf"},
description: "Portable Document Format",
filename: "internal-pdf-viewer",
length: 1,
name: "Chrome PDF Viewer"
},
{
0: {type: "application/x-nacl"},
description: "Native Client Executable",
filename: "internal-nacl-plugin",
length: 2,
name: "Native Client"
}
];
}
});
// Languages
Object.defineProperty(navigator, 'languages', {
get: () => ['en-US', 'en']
});
// Platform
Object.defineProperty(navigator, 'platform', {
get: () => 'Win32'
});
// Vendor
Object.defineProperty(navigator, 'vendor', {
get: () => 'Google Inc.'
});
// Removing Selenium traces
delete window.cdc_adoQpoasnfa76pfcZLmcfl_Array;
delete window.cdc_adoQpoasnfa76pfcZLmcfl_Promise;
delete window.cdc_adoQpoasnfa76pfcZLmcfl_Symbol;
// Battery API (not present in headless)
if (!navigator.getBattery) {
navigator.getBattery = () => Promise.resolve({
charging: true,
chargingTime: 0,
dischargingTime: Infinity,
level: 1
});
}
`;
Removing WebDriver Properties
ChromeDriver and other WebDriver implementations add specific variables to the global scope. These variables start with the prefix cdc_ and are easily detected by protection systems.
Detecting cdc Variables
You can check for the presence of these variables with a simple script:
// Searching for all cdc variables
for (let key in window) {
if (key.includes('cdc_')) {
console.log('WebDriver variable detected:', key);
}
}
// Typical ChromeDriver variables:
// cdc_adoQpoasnfa76pfcZLmcfl_Array
// cdc_adoQpoasnfa76pfcZLmcfl_Promise
// cdc_adoQpoasnfa76pfcZLmcfl_Symbol
// $cdc_asdjflasutopfhvcZLmcfl_
Method 1: Removing via CDP
The most reliable way is to remove the variables before the page loads using the Chrome DevTools Protocol:
from selenium import webdriver
driver = webdriver.Chrome()
# Removing all cdc variables
driver.execute_cdp_cmd('Page.addScriptToEvaluateOnNewDocument', {
'source': '''
// Removing known cdc variables
const cdcProps = [
'cdc_adoQpoasnfa76pfcZLmcfl_Array',
'cdc_adoQpoasnfa76pfcZLmcfl_Promise',
'cdc_adoQpoasnfa76pfcZLmcfl_Symbol',
'$cdc_asdjflasutopfhvcZLmcfl_'
];
cdcProps.forEach(prop => {
delete window[prop];
});
// Removing all variables containing 'cdc_'
Object.keys(window).forEach(key => {
if (key.includes('cdc_') || key.includes('$cdc_')) {
delete window[key];
}
});
'''
})
Method 2: Modifying ChromeDriver
A more radical approach is to modify the ChromeDriver binary file by replacing the cdc_ string with another character sequence. This prevents the creation of these variables:
import re
def patch_chromedriver(driver_path):
"""
Patches ChromeDriver by replacing 'cdc_' with a random string
"""
with open(driver_path, 'rb') as f:
content = f.read()
# Replacing all occurrences of 'cdc_' with 'dog_' (or any other string of the same length)
patched = content.replace(b'cdc_', b'dog_')
with open(driver_path, 'wb') as f:
f.write(patched)
print(f'ChromeDriver patched: {driver_path}')
# Usage
patch_chromedriver('/path/to/chromedriver')
Warning: Modifying the ChromeDriver binary file may disrupt its functionality. Always back up before patching. This method does not work with all versions of ChromeDriver.
Method 3: Using undetected-chromedriver
The undetected-chromedriver library automatically patches ChromeDriver upon launch:
pip install undetected-chromedriver
import undetected_chromedriver as uc
# Creating a driver with automatic patching
driver = uc.Chrome()
driver.get('https://nowsecure.nl/') # Site for detection testing
input('Press Enter to close...')
driver.quit()
Masking Canvas, WebGL, and Audio API
Canvas, WebGL, and Audio Fingerprinting are methods of creating a unique fingerprint based on graphics rendering and audio processing characteristics. Each combination of browser, OS, and hardware yields a unique result.
Canvas Fingerprinting
Systems draw a hidden image on the Canvas and analyze the resulting pixels. Headless browsers often yield atypical results due to the absence of GPU acceleration.
// Typical Canvas Fingerprinting code
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
ctx.textBaseline = 'top';
ctx.font = '14px Arial';
ctx.fillText('Browser fingerprint', 2, 2);
const fingerprint = canvas.toDataURL();
To protect against this, you can add random noise to the Canvas API:
const canvasNoiseScript = `
// Adding random noise to Canvas
const originalToDataURL = HTMLCanvasElement.prototype.toDataURL;
const originalToBlob = HTMLCanvasElement.prototype.toBlob;
const originalGetImageData = CanvasRenderingContext2D.prototype.getImageData;
// Noise adding function
const addNoise = (canvas, context) => {
const imageData = originalGetImageData.call(context, 0, 0, canvas.width, canvas.height);
for (let i = 0; i < imageData.data.length; i += 4) {
// Adding minimal noise to RGB (not noticeable to the eye)
imageData.data[i] += Math.floor(Math.random() * 3) - 1;
imageData.data[i + 1] += Math.floor(Math.random() * 3) - 1;
imageData.data[i + 2] += Math.floor(Math.random() * 3) - 1;
}
context.putImageData(imageData, 0, 0);
};
// Overriding toDataURL
HTMLCanvasElement.prototype.toDataURL = function() {
if (this.width > 0 && this.height > 0) {
const context = this.getContext('2d');
addNoise(this, context);
}
return originalToDataURL.apply(this, arguments);
};
`;
WebGL Fingerprinting
WebGL provides information about the graphics card and drivers. Headless browsers often show SwiftShader (software renderer) instead of the real GPU:
const webglMaskScript = `
// Masking WebGL parameters
const getParameter = WebGLRenderingContext.prototype.getParameter;
WebGLRenderingContext.prototype.getParameter = function(parameter) {
// UNMASKED_VENDOR_WEBGL
if (parameter === 37445) {
return 'Intel Inc.';
}
// UNMASKED_RENDERER_WEBGL
if (parameter === 37446) {
return 'Intel Iris OpenGL Engine';
}
return getParameter.call(this, parameter);
};
// Also for WebGL2
if (typeof WebGL2RenderingContext !== 'undefined') {
const getParameter2 = WebGL2RenderingContext.prototype.getParameter;
WebGL2RenderingContext.prototype.getParameter = function(parameter) {
if (parameter === 37445) {
return 'Intel Inc.';
}
if (parameter === 37446) {
return 'Intel Iris OpenGL Engine';
}
return getParameter2.call(this, parameter);
};
}
`;
Audio Context Fingerprinting
The Audio API also provides a unique fingerprint. We add noise to audio processing:
const audioMaskScript = `
// Adding noise to Audio Context
const AudioContext = window.AudioContext || window.webkitAudioContext;
if (AudioContext) {
const originalCreateAnalyser = AudioContext.prototype.createAnalyser;
AudioContext.prototype.createAnalyser = function() {
const analyser = originalCreateAnalyser.call(this);
const originalGetFloatFrequencyData = analyser.getFloatFrequencyData;
analyser.getFloatFrequencyData = function(array) {
originalGetFloatFrequencyData.call(this, array);
// Adding minimal noise
for (let i = 0; i < array.length; i++) {
array[i] += Math.random() * 0.0001;
}
};
return analyser;
};
}
`;
Imitating Human Behavior
Even with perfect technical masking, bots reveal themselves through behavior. Machine learning systems analyze mouse movement patterns, action speeds, and event sequences.
Random Delays Between Actions
Never use fixed delays. Real users pause for varying durations:
import random
import time
def human_delay(min_seconds=1, max_seconds=3):
"""Random delay imitating a human"""
delay = random.uniform(min_seconds, max_seconds)
time.sleep(delay)
# Usage
driver.get('https://example.com')
human_delay(2, 4) # Pause for 2-4 seconds
element = driver.find_element(By.ID, 'search')
human_delay(0.5, 1.5) # Short pause before input
element.send_keys('search query')
human_delay(1, 2)
Smooth Mouse Movement
Bots move the mouse in straight lines or teleport the cursor. Real users create curved trajectories with acceleration and deceleration:
// Puppeteer: smooth mouse movement
async function humanMouseMove(page, targetX, targetY) {
const steps = 25; // Number of intermediate points
const currentPos = await page.evaluate(() => ({
x: window.mouseX || 0,
y: window.mouseY || 0
}));
for (let i = 0; i <= steps; i++) {
const t = i / steps;
// Using easing for smoothness
const ease = t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
const x = currentPos.x + (targetX - currentPos.x) * ease;
const y = currentPos.y + (targetY - currentPos.y) * ease;
await page.mouse.move(x, y);
await page.waitForTimeout(Math.random() * 10 + 5);
}
// Saving position
await page.evaluate((x, y) => {
window.mouseX = x;
window.mouseY = y;
}, targetX, targetY);
}
// Usage
await humanMouseMove(page, 500, 300);
await page.mouse.click(500, 300);
Natural Scrolling
Real users scroll smoothly, stopping to read content:
async function humanScroll(page) {
const scrollHeight = await page.evaluate(() => document.body.scrollHeight);
const viewportHeight = await page.evaluate(() => window.innerHeight);
let currentPosition = 0;
while (currentPosition < scrollHeight - viewportHeight) {
// Random scroll step (200-500px)
const scrollStep = Math.floor(Math.random() * 300) + 200;
currentPosition += scrollStep;
// Smooth scroll
await page.evaluate((pos) => {
window.scrollTo({
top: pos,
behavior: 'smooth'
});
}, currentPosition);
// Pause for "reading" content (1-3 seconds)
await page.waitForTimeout(Math.random() * 2000 + 1000);
}
}
// Usage
await page.goto('https://example.com');
await humanScroll(page);
Natural Text Input
People type at varying speeds, make typos, and correct them:
async function humanTypeText(page, selector, text) {
await page.click(selector);
for (let char of text) {
// Random delay between keystrokes (50-200ms)
const delay = Math.random() * 150 + 50;
await page.waitForTimeout(delay);
// 5% chance of a typo
if (Math.random() < 0.05) {
// Typing a random character
const wrongChar = String.fromCharCode(97 + Math.floor(Math.random() * 26));
await page.keyboard.type(wrongChar);
await page.waitForTimeout(100 + Math.random() * 100);
// Deleting (Backspace)
await page.keyboard.press('Backspace');
await page.waitForTimeout(50 + Math.random() * 50);
}
await page.keyboard.type(char);
}
}
// Usage
await humanTypeText(page, '#search-input', 'example search query');
Proxy Integration for Complete Anonymity
Masking the browser is useless if all requests come from a single IP address. Anti-bot protection systems track the number of requests from each IP and block suspicious activity. Proxies are a mandatory component of any serious automation.
Choosing the Type of Proxy
Different tasks require different types of proxies:
| Type of Proxy | Advantages | Application |
|---|---|---|
| Residential Proxies | High anonymity, less likely to be blocked | Web scraping, account creation |
| Datacenter Proxies | Fast and cost-effective | General browsing, testing |
| Mobile Proxies | High anonymity, simulates mobile devices | Social media automation, app testing |