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.

11 mins readPublished Updated
On this page
  1. What a headless browser is
  2. Why new headless changed detection
  3. The competitor's spy: Why they target your store
  4. Detection methods that work in 2026
  5. Detection layers compared
  6. Action plan for merchants
  7. Where to start
  8. Frequently asked questions

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.

LayerTechniqueWhat it Catches
Traditional WAFIP Blacklisting / Rate LimitingBasic, high-volume scrapers
Client-Side JSnavigator.webdriver checkAmateur Puppeteer scripts
Advanced FingerprintingCDP Detection & WebGL HashesProfessional New Headless bots
Sensfrx AIBehavioural BiometricsResidential 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.

  1. Find the endpoint. Bots go for the high-value paths: /checkout, and platform routes such as /wp-admin/admin-ajax.php. Secure those first.
  2. 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.
  3. Hide the backdoor. Redirect or password-protect aggregation pages such as /collections/all so research tools cannot scrape your winning products.
  4. 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.

Frequently asked questions

Eswar RB

Written by

Eswar RB

Associate product software engineer and technical content writer

Works on the Sensfrx detection models and documentation, with four years in machine learning and deep learning applied to fraud prevention.

LinkedIn

See how Sensfrx scores every signup, login and payment in real time.