A browser with no graphical interface that runs on a server and executes JavaScript and renders CSS like a normal browser. Attackers run Headless Chrome through Puppeteer or Playwright because it behaves like a person, which makes it effective for card testing, account takeover and competitor scraping.
Headless browser attacks: How to detect them in 2026
How Headless Chrome bots run card testing, account takeover and competitor scraping against ecommerce stores, and four detection methods with working code.
On this page
A bot used to be easy to spot: no cookies, an odd User-Agent string, and it moved with the grace of a brick. Attackers now run Headless Chrome through Puppeteer or Playwright, and it behaves like a person.
For ecommerce merchants and developers, these browsers are the main tool behind card testing, account takeover (ATO) and, for growing brands, competitor intelligence scraping.
What a headless browser is
A headless browser is a web browser with no graphical user interface (GUI). It is a full browser environment that runs JavaScript and renders CSS, but it runs on a server or from the command line.
- Faster and lighter, because no CPU or RAM goes on drawing pixels to a screen.
- Runs where there is no display: servers and CI/CD pipelines.
- Driven by code, through APIs or the command line.
Developers need them for automated testing. Fraudsters and competitor spies use the same tools.
Legitimate uses
Headless browsers do the browsing jobs that need no human at the keyboard:
- E2E testing: simulating user interactions to check frontend flows.
- Web automation: filling complex forms or working internal web tools at scale.
- Web scraping: single-page applications render content with JavaScript, so a plain GET request gets nothing.
Why new headless changed detection
Google replaced the old headless mode, a separate stripped-down build, with one that is the real Chrome browser running without a window. Checking for a headless User-Agent stopped working. If the browser is Chrome, how do you tell whether a person or a script is clicking the buttons?
The competitor's spy: Why they target your store
If your brand has gone viral on Instagram or you have scaled up Meta Ads, expect shadow traffic within 48 hours. Tools like Koala Inspector and PPSPY use headless browsers to scrape:
- Live sales data. They do not guess your revenue; they read your "recent purchase" pop-ups and inventory levels to see what you make each day.
- The /collections/all backdoor. Bots hit that URL with a sort_by=created-desc parameter to see your newest products before you market them.
- Inventory cart-testing. Bots add 999 items to a cart to leak your exact stock level, so a competitor knows when you are running low and can undercut your ads.
Detection methods that work in 2026
Security platforms like Sensfrx look for automation artefacts inside the browser's execution layer. Four methods, each with code.
1. User-agent and WebDriver flags
The simplest check looks for the automation flags headless browsers often expose.
Example code (server-side detection):
from flask import Flask, request
import re
app = Flask(__name__)
def detect_headless_browser(user_agent, headers):
"""Detect headless browsers through User-Agent and header analysis"""
# Common headless browser signatures
headless_patterns = [
r'headless',
r'phantomjs',
r'selenium',
r'puppeteer',
r'playwright'
]
# Check User-Agent
ua_lower = user_agent.lower()
for pattern in headless_patterns:
if re.search(pattern, ua_lower):
return True, f"Headless pattern detected: {pattern}"
# Check for missing common headers
common_headers = ['accept-language', 'accept-encoding']
missing_headers = [h for h in common_headers if h not in headers]
if len(missing_headers) > 0:
return True, f"Missing headers: {missing_headers}"
# Check for automation flags
webdriver_flag = headers.get('sec-ch-ua')
if webdriver_flag and 'Headless' in webdriver_flag:
return True, "Headless Chrome detected in sec-ch-ua"
return False, "Appears to be genuine browser"
@app.route('/api/check')
def check_request():
user_agent = request.headers.get('User-Agent', '')
is_bot, reason = detect_headless_browser(user_agent, request.headers)
return {
'is_bot': is_bot,
'reason': reason,
'user_agent': user_agent
}
if __name__ == '__main__':
app.run(debug=True)Result: When a headless browser visits this endpoint, the server sees patterns like "HeadlessChrome" in the User-Agent or a missing standard header such as Accept-Language. A Puppeteer bot might return: {‘is_bot’: True, ‘reason’: ‘Headless pattern detected: headless’, ‘user_agent’: ‘Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 HeadlessChrome/120.0.0.0’}.
2. Behavioral timing analysis
Bots move in straight lines or "teleport" between coordinates. Human mouse movement is curved, with variable acceleration.
Example code (analyzing movement data):
import numpy as np
from scipy.spatial.distance import euclidean
import json
def analyse_mouse_movements(movement_data):
"""
Analyse mouse movement patterns to detect bots
movement_data: list of dicts with x, y, timestamp
"""
if len(movement_data) < 10:
return False, "Insufficient data"
# Extract coordinates and timestamps
points = [(m['x'], m['y']) for m in movement_data]
times = [m['time'] for m in movement_data]
# Calculate velocities and accelerations
velocities = []
for i in range(1, len(points)):
dist = euclidean(points[i], points[i-1])
time_diff = (times[i] - times[i-1]) / 1000.0 # Convert to seconds
if time_diff > 0:
velocity = dist / time_diff
velocities.append(velocity)
# Check for perfect straight lines (collinearity)
straight_line_count = 0
for i in range(2, len(points)):
p1, p2, p3 = points[i-2], points[i-1], points[i]
# Calculate cross product to check collinearity
cross_product = abs(
(p2[0] - p1[0]) * (p3[1] - p1[1]) -
(p2[1] - p1[1]) * (p3[0] - p1[0])
)
if cross_product < 1.0: # Nearly perfect line
straight_line_count += 1
linearity_ratio = straight_line_count / (len(points) - 2)
# Check for constant velocity (bot signature)
if len(velocities) > 5:
velocity_variance = np.var(velocities)
velocity_mean = np.mean(velocities)
# Human movements have high variance
if velocity_variance < 100 and velocity_mean > 500:
return True, f"Constant velocity detected (var: {velocity_variance:.2f})"
# Check linearity
if linearity_ratio > 0.7:
return True, f"Too linear: {linearity_ratio*100:.1f}% straight lines"
return False, "Movement appears human"
# Example usage
movement_data = [
{'x': 100, 'y': 100, 'time': 1000},
{'x': 150, 'y': 150, 'time': 1050},
{'x': 200, 'y': 200, 'time': 1100},
{'x': 250, 'y': 250, 'time': 1150},
{'x': 300, 'y': 300, 'time': 1200},
{'x': 350, 'y': 350, 'time': 1250},
{'x': 400, 'y': 400, 'time': 1300},
{'x': 450, 'y': 450, 'time': 1350},
{'x': 500, 'y': 500, 'time': 1400},
{'x': 550, 'y': 550, 'time': 1450},
]
is_bot, reason = analyse_mouse_movements(movement_data)
print(f"Bot detected: {is_bot}")
print(f"Reason: {reason}")Result: For the example data, a perfect diagonal at constant velocity, the output is: Bot detected: True, Reason: Too linear: 100.0% straight lines. People produce curves with variable acceleration; bots produce geometry.
3. Hardware and API inconsistencies
Most headless servers have no GPU and no sound card. Collect the browser fingerprint on the client, validate it on the server, and the mismatches show.
Example code (fingerprint analysis):
import hashlib
from collections import Counter
class FingerprintAnalyser:
def __init__(self):
# Known headless signatures
self.headless_renderers = [
'swiftshader',
'llvmpipe',
'mesa',
'angle'
]
self.headless_vendors = [
'google inc. (intel)',
'brian paul',
'vmware'
]
def analyse_fingerprint(self, fingerprint_data):
"""
Analyse browser fingerprint for inconsistencies
fingerprint_data: dict with webgl_renderer, webgl_vendor,
platform, user_agent, audio_devices
"""
flags = []
renderer = fingerprint_data.get('webgl_renderer', '').lower()
vendor = fingerprint_data.get('webgl_vendor', '').lower()
platform = fingerprint_data.get('platform', '')
user_agent = fingerprint_data.get('user_agent', '')
audio_count = fingerprint_data.get('audio_devices', 0)
# Check for headless GPU signatures
for sig in self.headless_renderers:
if sig in renderer:
flags.append(f"Headless GPU detected: {sig}")
for sig in self.headless_vendors:
if sig in vendor:
flags.append(f"Headless vendor detected: {sig}")
# Check for hardware inconsistencies
if 'mac' in user_agent.lower() or 'macintosh' in platform.lower():
if 'swiftshader' in renderer or 'llvmpipe' in renderer:
flags.append("MacOS claimed but software renderer detected")
if audio_count == 0:
flags.append("MacOS claimed but no audio devices")
if 'windows' in user_agent.lower() or 'win' in platform.lower():
if 'llvmpipe' in renderer:
flags.append("Windows claimed but Linux renderer detected")
# Check canvas consistency
canvas_hash = fingerprint_data.get('canvas_hash', '')
if canvas_hash in self.get_known_headless_hashes():
flags.append("Known headless canvas signature")
is_bot = len(flags) > 0
return is_bot, flags
def get_known_headless_hashes(self):
"""Returns known headless browser canvas hashes"""
return [
'a3c4b8f9e2d1', # Common Puppeteer signature
'f7e8d9c6b5a4', # Common Playwright signature
]
# Example usage
analyser = FingerprintAnalyser()
# Suspicious fingerprint (headless browser)
suspicious_fp = {
'webgl_renderer': 'ANGLE (Intel, Mesa DRI Intel(R) HD Graphics, SwiftShader)',
'webgl_vendor': 'Google Inc. (Intel)',
'platform': 'MacIntel',
'user_agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)',
'audio_devices': 0,
'canvas_hash': 'a3c4b8f9e2d1'
}
is_bot, flags = analyser.analyse_fingerprint(suspicious_fp)
print(f"Bot detected: {is_bot}")
print(f"Flags: {flags}")
# Genuine fingerprint
genuine_fp = {
'webgl_renderer': 'ANGLE (Apple, Apple M1, OpenGL 4.1)',
'webgl_vendor': 'Google Inc. (Apple)',
'platform': 'MacIntel',
'user_agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)',
'audio_devices': 2,
'canvas_hash': '8d7c6b5a4e3f'
}
is_bot2, flags2 = analyser.analyse_fingerprint(genuine_fp)
print(f"\nGenuine browser - Bot detected: {is_bot2}")
print(f"Flags: {flags2}")Result: For the suspicious fingerprint:
Bot detected: True
Flags: [‘Headless GPU detected: swiftshader’, ‘MacOS claimed but software renderer detected’, ‘MacOS claimed but no audio devices’, ‘Known headless canvas signature’]
For the genuine fingerprint: Bot detected: False, Flags: []
4. Request pattern analysis for scraping
Scrapers leave distinctive request patterns on an ecommerce site.
Example code (traffic pattern analysis):
from datetime import datetime, timedelta
from collections import defaultdict
import re
class ScraperDetector:
def __init__(self):
self.request_log = defaultdict(list)
self.suspicious_patterns = {
'collections_backdoor': r'/collections/all\?.\\*sort_by=created',
'cart_testing': r'/cart/add.\\*quantity=(999|9999)',
'rapid_product_viewing': r'/products/',
'admin_probing': r'/wp-admin|/admin'
}
def log_request(self, ip_address, path, timestamp=None):
"""Log incoming requests for pattern analysis"""
if timestamp is None:
timestamp = datetime.now()
self.request_log[ip_address].append({
'path': path,
'time': timestamp
})
def analyse_ip(self, ip_address, time_window_minutes=5):
"""Analyse request patterns for a specific IP"""
if ip_address not in self.request_log:
return False, "No data"
requests = self.request_log[ip_address]
now = datetime.now()
recent_requests = [
r for r in requests
if (now - r['time']).total_seconds() < time_window_minutes * 60
]
if len(recent_requests) < 5:
return False, "Insufficient activity"
flags = []
# Check for backdoor access
backdoor_hits = [
r for r in recent_requests
if re.search(self.suspicious_patterns['collections_backdoor'], r['path'])
]
if len(backdoor_hits) > 0:
flags.append(f"Collections backdoor accessed {len(backdoor_hits)} times")
# Check for cart testing
cart_tests = [
r for r in recent_requests
if re.search(self.suspicious_patterns['cart_testing'], r['path'])
]
if len(cart_tests) > 0:
flags.append(f"Inventory cart testing detected ({len(cart_tests)} attempts)")
# Check request rate
request_rate = len(recent_requests) / time_window_minutes
if request_rate > 20: # More than 20 requests per minute
flags.append(f"High request rate: {request_rate:.1f} req/min")
# Check for sequential product browsing (bot pattern)
product_paths = [
r['path'] for r in recent_requests
if '/products/' in r['path']
]
if len(product_paths) > 10:
# Check if products are being accessed too quickly
product_times = [
r['time'] for r in recent_requests
if '/products/' in r['path']
]
if len(product_times) >= 2:
avg_time_between = sum([
(product_times[i] - product_times[i-1]).total_seconds()
for i in range(1, len(product_times))
]) / (len(product_times) - 1)
if avg_time_between < 2: # Less than 2 seconds between products
flags.append(f"Rapid product scanning: {avg_time_between:.2f}s avg")
is_scraper = len(flags) > 0
return is_scraper, flags
# Example usage
detector = ScraperDetector()
# Simulate bot activity
bot_ip = "203.0.113.45"
base_time = datetime.now()
# Bot accessing products rapidly
for i in range(15):
detector.log_request(
bot_ip,
f"/products/product-{i}",
base_time + timedelta(seconds=i*1.5)
)
# Bot using backdoor
detector.log_request(bot_ip, "/collections/all?sort_by=created-desc", base_time)
# Bot testing inventory
detector.log_request(bot_ip, "/cart/add?id=12345&quantity=999", base_time)
is_scraper, flags = detector.analyse_ip(bot_ip)
print(f"Scraper detected: {is_scraper}")
print(f"Flags:")
for flag in flags:
print(f" - {flag}")
# Simulate normal user
normal_ip = "198.51.100.23"
detector.log_request(normal_ip, "/", base_time)
detector.log_request(normal_ip, "/products/shirt-1", base_time + timedelta(seconds=30))
detector.log_request(normal_ip, "/products/shirt-1", base_time + timedelta(seconds=45))
detector.log_request(normal_ip, "/cart", base_time + timedelta(seconds=60))
is_scraper2, flags2 = detector.analyse_ip(normal_ip)
print(f"\nNormal user - Scraper detected: {is_scraper2}")
print(f"Reason: {flags2}")Result: For the bot IP:
Scraper detected: True
Flags:
- Collections backdoor accessed 1 times
- Inventory cart testing detected (1 attempts)
- High request rate: 3.4 req/min
- Rapid product scanning: 1.50s avg
For the normal user: Scraper detected: False, Reason: Insufficient activity
Detection layers compared
The table sets the layers side by side: the technique at each level and the traffic it catches.
| Layer | Technique | What it Catches |
|---|---|---|
| Traditional WAF | IP Blacklisting / Rate Limiting | Basic, high-volume scrapers |
| Client-Side JS | navigator.webdriver check | Amateur Puppeteer scripts |
| Advanced Fingerprinting | CDP Detection & WebGL Hashes | Professional New Headless bots |
| Sensfrx AI | Behavioural Biometrics | Residential Proxies & Spy Extensions |
Action plan for merchants
If your WooCommerce or Shopify store is seeing a rise in card testing, or in non-human "ghost visitor" traffic from data centers, do four things.
- Find the endpoint. Bots go for the high-value paths: /checkout, and platform routes such as /wp-admin/admin-ajax.php. Secure those first.
- Muzzle notifications. During a card-testing run, turn off the automated "failed order" emails. The flood damages your server's sender reputation and gets it marked as spam.
- Hide the backdoor. Redirect or password-protect aggregation pages such as /collections/all so research tools cannot scrape your winning products.
- Move to behavioral defense. Static rules such as IP blacklisting do not catch a well-built botnet. Use behavioral biometrics on the checkout funnel: how the visitor interacts, not where their IP sits.
Where to start
Automated attacks on ecommerce stores will keep improving, and the User-Agent check is already beaten. Detection that reads behavior and context, the four methods above, is what protects the checkout and the product data. Ship method 1 first because it is the quickest, then add timing analysis and fingerprint validation on the checkout.
