NuData Security's behavioral biometrics system blocks sophisticated automation attempts on Ticketmaster, Kohl's, and major enterprise platforms. This guide provides battle-tested techniques to bypass their detection in 2026.
NuData bypassing requires understanding three detection layers: behavioral biometrics, device fingerprinting, and the Trust Consortium network. The critical nds-pmd token validates your session—without proper generation, requests get flagged instantly. This guide covers extraction methods, token generation, and human-like behavior simulation.
Understanding NuData's Detection Architecture
NuData, acquired by Mastercard in 2017, monitors over 650 billion behavioral events annually. Their NuDetect product uses machine learning models analyzing three core data layers.
The first layer tracks behavioral biometrics. This includes mouse movements, typing patterns, scrolling behavior, and device angle information from accelerometers.
The second layer handles device fingerprinting. Canvas fingerprints, WebGL parameters, screen resolution, installed fonts, and audio context all contribute to device identification.
The third layer provides network intelligence. IP reputation scoring, geolocation consistency checks, and cross-client data from the Trust Consortium flag suspicious patterns.
Understanding these layers matters because you need to defeat all three simultaneously. Fooling device fingerprinting means nothing if your typing pattern screams "bot."
What's New in NuData 2026
NuData's detection capabilities have evolved significantly. Here are the changes you'll face in 2026.
CDP Protocol Detection is now standard. Anti-bot systems detect the Chrome DevTools Protocol commands that Puppeteer, Playwright, and Selenium use. Traditional stealth plugins no longer work against aggressive implementations.
Trust Consortium v3 shares threat intelligence across 4.5 billion monitored devices. If your fingerprint or IP gets flagged on one client's site, you're flagged everywhere.
Hybrid Attack Detection catches combinations of automation and human involvement. Even using human farms to solve CAPTCHAs triggers behavioral anomaly flags.
Real-Time ML Scoring analyzes hundreds of features within milliseconds. Static fingerprints that worked in 2024 now get caught by drift detection.
Step 1: Extract the NuData Script ID
Before generating tokens, locate NuData's JavaScript on your target site. The script lives at *.nudatasecurity.com and contains configuration data.
Method A: Request-Based Extraction (Lightweight)
For high-volume operations, extract the script ID directly from HTML:
import re
import requests
from bs4 import BeautifulSoup
def extract_nudata_script(url):
"""
Extracts NuData script ID from target webpage.
Returns tuple of (script_id, session_params)
"""
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.5',
'Accept-Encoding': 'gzip, deflate, br',
'DNT': '1',
'Connection': 'keep-alive',
'Upgrade-Insecure-Requests': '1'
}
response = requests.get(url, headers=headers)
soup = BeautifulSoup(response.text, 'html.parser')
This code sets up a requests session with browser-like headers. The DNT: 1 header signals "Do Not Track" which real browsers often send.
# Pattern 1: Direct script tag
script_pattern = re.compile(
r'<script[^>]*\s+src=["\'].*?nudatasecurity\.com/([^"\']+)["\'][^>]*>',
re.IGNORECASE
)
# Pattern 2: Dynamic script loading
dynamic_pattern = re.compile(
r'loadScript\(["\'].*?nudatasecurity\.com/([^"\']+)["\']',
re.IGNORECASE
)
script_match = script_pattern.search(response.text)
if not script_match:
script_match = dynamic_pattern.search(response.text)
The regex patterns catch both inline script tags and dynamically loaded scripts. Some sites lazy-load NuData after initial page render.
if script_match:
script_id = script_match.group(1)
# Extract session parameters if available
session_params = {}
session_pattern = re.compile(
r'nds[_-]?session[_-]?id["\']?\s*[:=]\s*["\']([^"\']+)'
)
session_match = session_pattern.search(response.text)
if session_match:
session_params['session_id'] = session_match.group(1)
return script_id, session_params
return None, {}
Session IDs appear in page source for sites that pre-generate them. Capturing these prevents mismatched session errors later.
Method B: Browser-Based Extraction with Nodriver
For heavily obfuscated sites, use a real browser. Nodriver is the 2025 successor to undetected-chromedriver with better CDP evasion:
import nodriver as uc
import asyncio
import re
async def extract_nudata_browser(url):
"""
Uses Nodriver to extract NuData configuration from runtime.
Nodriver avoids CDP detection unlike Puppeteer/Playwright.
"""
browser = await uc.start(
headless=False, # Headed mode evades more checks
browser_args=[
'--disable-blink-features=AutomationControlled',
'--disable-dev-shm-usage',
'--no-sandbox'
]
)
nudata_config = {}
page = await browser.get(url)
await page.sleep(3) # Give NuData time to initialize
Nodriver communicates directly with Chrome without the standard WebDriver protocol. This eliminates detection vectors that flag Selenium and Puppeteer.
# Extract script URL from network requests
# Nodriver doesn't have request interception, so we check DOM
scripts = await page.select_all('script[src*="nudatasecurity"]')
for script in scripts:
src = await script.get_attribute('src')
if src:
nudata_config['script_url'] = src
match = re.search(r'nudatasecurity\.com/([^?]+)', src)
if match:
nudata_config['script_id'] = match.group(1)
This loop finds all NuData script elements in the DOM. Some sites load multiple scripts for different tracking purposes.
# Extract runtime configuration from window object
try:
runtime_config = await page.evaluate("""
() => {
const config = {};
if (window.ndsObj) config.ndsObj = JSON.stringify(window.ndsObj);
if (window.nds) config.nds = JSON.stringify(window.nds);
if (window.__NDS_CONFIG__) {
config.__NDS_CONFIG__ = JSON.stringify(window.__NDS_CONFIG__);
}
return config;
}
""")
nudata_config['runtime'] = runtime_config
except Exception as e:
print(f"Could not extract runtime config: {e}")
await browser.stop()
return nudata_config
# Usage
config = asyncio.run(extract_nudata_browser('https://www.ticketmaster.com'))
print(config)
The JavaScript evaluation pulls NuData's configuration objects from the browser's window scope. These contain encryption keys and session parameters.
Step 2: Generate Valid NDS-PMD Tokens
The nds-pmd parameter contains encoded behavioral data that NuData analyzes. Three methods exist for generation.
Method A: Local Token Generation
Generate tokens locally by reverse-engineering NuData's encoding:
import hashlib
import base64
import json
import time
import random
class LocalNuDataGenerator:
def __init__(self):
self.behavioral_buffer = []
self.start_time = int(time.time() * 1000)
The class maintains a behavioral buffer that accumulates interaction data over time, mimicking how real users browse.
def generate_local_token(self, config):
"""
Generates NuData token locally using behavioral simulation.
This replicates NuData's encoding without needing their script.
"""
profile = {
'jvqtrgQngn': { # Obfuscated field name (ROT13 encoded)
'oq': self._get_screen_info(),
'wfi': f"flap-{random.randint(100000, 999999)}",
'oc': self._generate_session_hash(),
'fe': f"{1920}k{1080} 24",
'qvqgm': str(random.randint(250, 350)),
'wxe': int(time.time() * 1000) - self.start_time,
'syi': 'snyfr', # ROT13 for 'false'
'si': 'si,btt,zc4,jroz',
'sn': 'sn,zcrt,btt,jni',
'us': self._generate_device_hash(),
'cy': base64.b64encode('MacIntel'.encode()).decode(),
'sg': json.dumps({'zgc': 0, 'gf': False, 'gr': False}),
'sp': json.dumps({'gp': True, 'ap': True}),
'sf': 'gehr', # ROT13 for 'true'
'jt': self._generate_session_hash(),
'sz': self._generate_device_hash(),
'vce': self._encode_interactions()
}
}
encoded = base64.b64encode(
json.dumps(profile).encode()
).decode()
return encoded
NuData uses ROT13 and base64 encoding for field names. The vce field contains encoded interaction events that prove human behavior.
def _get_screen_info(self):
"""Generate realistic screen dimension string"""
screens = [
"1920:1080:1920:1040:1920:1080",
"1440:900:1440:860:1440:900",
"2560:1440:2560:1400:2560:1440",
"1366:768:1366:728:1366:768",
"3840:2160:3840:2120:3840:2160"
]
return random.choice(screens)
def _generate_session_hash(self):
"""Generate consistent session hash"""
data = f"{time.time()}{random.random()}"
return hashlib.md5(data.encode()).hexdigest()[:16]
def _generate_device_hash(self):
"""Generate device fingerprint hash"""
device_data = {
'platform': 'Win32',
'vendor': 'Google Inc.',
'renderer': 'ANGLE (Intel, Intel(R) UHD Graphics Direct3D11)',
'memory': 8
}
return hashlib.sha256(
json.dumps(device_data).encode()
).hexdigest()[:16]
These helper methods create realistic device signatures. The screen options cover common resolutions weighted toward popular sizes.
def _encode_interactions(self):
"""Encode behavioral interaction data"""
interactions = []
# Mouse movements - generate realistic cursor paths
for i in range(random.randint(15, 35)):
interactions.append(
f"mm,{random.randint(100, 1900)},{random.randint(100, 1000)}"
)
# Key presses - vary timing between keys
for i in range(random.randint(8, 20)):
interactions.append(
f"kp,{random.randint(65, 90)},{random.randint(80, 200)}"
)
# Scroll events - humans scroll in bursts
for i in range(random.randint(3, 10)):
interactions.append(
f"sc,{random.randint(0, 3000)},{random.randint(1, 5)}"
)
return ';'.join(interactions)
def add_interaction(self, action_type, data):
"""Add behavioral interaction to buffer"""
self.behavioral_buffer.append({
'type': action_type,
'data': data,
'timestamp': int(time.time() * 1000)
})
The interaction encoder creates mouse movement, keypress, and scroll events. Real humans generate 20-50 events per session.
Method B: Browser-Executed Token Generation
Let the actual NuData script generate tokens, then intercept them:
import nodriver as uc
import asyncio
async def capture_nudata_token(url, form_selector):
"""
Navigate to page, interact naturally, and capture generated token.
This uses NuData's own script to create valid tokens.
"""
browser = await uc.start(headless=False)
page = await browser.get(url)
await page.sleep(2)
# Simulate human browsing
await page.scroll_down(150)
await page.sleep(random.uniform(0.5, 1.5))
await page.scroll_down(100)
Starting with scrolling before any form interaction matches human behavior. Users read page content before filling forms.
# Find and interact with form
form = await page.select(form_selector)
if form:
# Move to form area with human-like timing
await page.sleep(random.uniform(0.3, 0.8))
# Type with realistic cadence
input_field = await page.select('input[type="text"]')
if input_field:
await input_field.click()
await page.sleep(0.2)
# Type each character with variable delay
text = "test@example.com"
for char in text:
await page.send_keys(char)
await page.sleep(random.gauss(0.1, 0.03))
The typing simulation uses Gaussian distribution for keystroke timing. This matches natural human typing patterns where speed varies around an average.
# Wait for NuData to process interactions
await page.sleep(3)
# Extract generated token
token = await page.evaluate("""
() => {
// Check form hidden fields
const ndsPmd = document.querySelector('input[name="nds-pmd"]');
if (ndsPmd) return ndsPmd.value;
// Check window object
if (window.ndsToken) return window.ndsToken;
if (window.nds && window.nds.getToken) return window.nds.getToken();
return null;
}
""")
await browser.stop()
return token
# Usage
token = asyncio.run(capture_nudata_token(
'https://www.ticketmaster.com/login',
'form#login-form'
))
NuData injects tokens into hidden form fields or window objects before submission. Both locations need checking.
Step 3: Choose Your Bypass Strategy
Different targets require different approaches. Here's when to use each strategy.
Strategy 1: Pure Requests (Fastest, Riskiest)
Best for APIs with minimal behavioral checks:
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
import random
class NuDataRequester:
def __init__(self, proxies=None):
self.session = requests.Session()
self.proxies = proxies or []
# Configure retry with exponential backoff
retry_strategy = Retry(
total=3,
backoff_factor=1,
status_forcelist=[429, 500, 502, 503, 504]
)
adapter = HTTPAdapter(max_retries=retry_strategy)
self.session.mount("http://", adapter)
self.session.mount("https://", adapter)
Retry configuration handles transient failures. The backoff factor prevents hammering servers during outages.
def _get_browser_headers(self):
"""
Returns browser-like headers with proper ordering.
Header order matters for TLS fingerprinting.
"""
return {
'sec-ch-ua': '"Chromium";v="131", "Not_A Brand";v="24", "Google Chrome";v="131"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"Windows"',
'Upgrade-Insecure-Requests': '1',
'User-Agent': self._get_random_ua(),
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
'Sec-Fetch-Site': 'none',
'Sec-Fetch-Mode': 'navigate',
'Sec-Fetch-User': '?1',
'Sec-Fetch-Dest': 'document',
'Accept-Encoding': 'gzip, deflate, br',
'Accept-Language': 'en-US,en;q=0.9'
}
The sec-ch-ua headers must match your claimed browser version. Mismatches between User-Agent and Client Hints trigger flags.
def _get_random_ua(self):
"""Returns random realistic user agent"""
user_agents = [
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36'
]
return random.choice(user_agents)
def make_request(self, url, nds_token, method='GET', data=None):
"""Makes request with NuData token"""
proxy = random.choice(self.proxies) if self.proxies else None
self.session.headers.update(self._get_browser_headers())
if method == 'POST':
if data is None:
data = {}
data['nds-pmd'] = nds_token
response = self.session.post(
url,
data=data,
proxies={'http': proxy, 'https': proxy} if proxy else None,
timeout=30
)
else:
params = {'nds-pmd': nds_token}
response = self.session.get(
url,
params=params,
proxies={'http': proxy, 'https': proxy} if proxy else None,
timeout=30
)
return response
Proxy rotation via Roundproxies residential proxies prevents IP-based blocking. Datacenter IPs often get flagged immediately by NuData's network intelligence.
Strategy 2: Nodriver (Best Balance)
Nodriver avoids CDP detection while maintaining automation capabilities:
import nodriver as uc
import asyncio
import random
class NodriverBypass:
def __init__(self, proxy=None):
self.proxy = proxy
self.browser = None
async def start(self):
"""Initialize browser with stealth settings"""
browser_args = [
'--disable-blink-features=AutomationControlled',
'--disable-dev-shm-usage',
'--no-sandbox',
'--disable-gpu',
'--window-size=1920,1080'
]
if self.proxy:
browser_args.append(f'--proxy-server={self.proxy}')
self.browser = await uc.start(
headless=False, # Headed mode is less detectable
browser_args=browser_args
)
return self
Nodriver's architecture avoids the Runtime.enable CDP command that anti-bots detect. This single change defeats many 2025-era detections.
async def navigate_with_behavior(self, url):
"""Navigate with human-like behavior patterns"""
page = await self.browser.get(url)
# Initial page load wait
await page.sleep(random.uniform(1.5, 3.0))
# Simulated reading - scroll in bursts
scroll_count = random.randint(3, 7)
for _ in range(scroll_count):
scroll_amount = random.randint(100, 400)
await page.scroll_down(scroll_amount)
# Reading pause between scrolls
await page.sleep(random.uniform(0.8, 2.5))
return page
Scroll patterns matter. Bots scroll smoothly and continuously. Humans scroll, stop to read, then scroll again in variable increments.
async def type_humanlike(self, element, text):
"""Type with human-like cadence and occasional mistakes"""
await element.click()
await asyncio.sleep(random.uniform(0.2, 0.5))
for i, char in enumerate(text):
# Occasional typo (2% chance)
if random.random() < 0.02 and char.isalpha():
wrong = random.choice('abcdefghijklmnopqrstuvwxyz')
await self.browser.send_keys(wrong)
await asyncio.sleep(random.uniform(0.2, 0.4))
await self.browser.send_keys('\b') # Backspace
await asyncio.sleep(random.uniform(0.1, 0.2))
await self.browser.send_keys(char)
# Variable delay between keystrokes
if char == ' ':
delay = random.uniform(0.1, 0.3)
elif char in '.,!?':
delay = random.uniform(0.2, 0.5)
else:
delay = random.gauss(0.1, 0.03)
await asyncio.sleep(max(0.05, delay))
async def close(self):
if self.browser:
await self.browser.stop()
Typo simulation and correction dramatically increases behavioral realism. NuData specifically looks for too-perfect typing.
Strategy 3: SeleniumBase CDP Mode (Hybrid Approach)
SeleniumBase combines WebDriver convenience with CDP stealth:
from seleniumbase import SB
def bypass_with_seleniumbase(url, target_selector):
"""
Uses SeleniumBase CDP mode for NuData bypass.
CDP mode avoids WebDriver detection while keeping simple API.
"""
with SB(uc=True, test=True, locale="en") as sb:
# Activate CDP mode - disconnects WebDriver
sb.activate_cdp_mode(url)
sb.sleep(2)
# Check for Cloudflare challenge first
if sb.is_element_visible('iframe[src*="challenges"]'):
sb.uc_gui_click_captcha()
sb.sleep(3)
CDP mode disconnects the WebDriver protocol while maintaining browser control. This hybrid approach defeats both WebDriver and CDP detection.
# Navigate with CDP methods
sb.cdp.scroll_down(200)
sb.sleep(random.uniform(1, 2))
# Find and interact with elements
if sb.cdp.is_element_visible(target_selector):
sb.cdp.click(target_selector)
sb.sleep(0.5)
# Type using CDP - bypasses automation detection
input_selector = 'input[type="email"]'
if sb.cdp.is_element_visible(input_selector):
sb.cdp.press_keys(input_selector, 'user@example.com')
sb.sleep(random.uniform(1, 2))
# Get page content
html = sb.cdp.get_page_source()
return html
SeleniumBase's CDP methods use PyAutoGUI for actual mouse/keyboard input, making actions indistinguishable from human input at the OS level.
Step 4: Implement Human-Like Behavior
NuData detects bots through behavioral analysis. Your automation must generate realistic patterns.
Advanced Mouse Movement Simulation
import numpy as np
from scipy import interpolate
import time
import random
class MouseSimulator:
def __init__(self):
self.profile = self._generate_profile()
def _generate_profile(self):
"""Generate unique mouse movement profile"""
return {
'avg_speed': np.random.normal(400, 100),
'acceleration': np.random.normal(1.5, 0.3),
'jitter_amplitude': np.random.uniform(1, 3),
'overshoot_probability': 0.15,
'micro_pause_probability': 0.1
}
Each session gets a unique movement profile. Consistent profiles across sessions would themselves become a fingerprint.
def generate_bezier_path(self, start, end, num_points=None):
"""
Generate realistic mouse path using cubic Bezier curves.
Humans move in curves, not straight lines.
"""
start_x, start_y = start
end_x, end_y = end
distance = np.sqrt((end_x - start_x)**2 + (end_y - start_y)**2)
if num_points is None:
num_points = max(10, int(distance / 50))
# Control points create natural curve
ctrl1_x = start_x + (end_x - start_x) * 0.25 + random.uniform(-50, 50)
ctrl1_y = start_y + (end_y - start_y) * 0.25 + random.uniform(-50, 50)
ctrl2_x = start_x + (end_x - start_x) * 0.75 + random.uniform(-50, 50)
ctrl2_y = start_y + (end_y - start_y) * 0.75 + random.uniform(-50, 50)
Bezier curves create the natural arcs humans make when moving to targets. Control point randomization ensures unique paths.
# Generate curve points
t = np.linspace(0, 1, num_points)
x = ((1-t)**3 * start_x +
3*(1-t)**2*t * ctrl1_x +
3*(1-t)*t**2 * ctrl2_x +
t**3 * end_x)
y = ((1-t)**3 * start_y +
3*(1-t)**2*t * ctrl1_y +
3*(1-t)*t**2 * ctrl2_y +
t**3 * end_y)
# Add micro-jitter (hand tremor)
x += np.random.normal(0, self.profile['jitter_amplitude'], num_points)
y += np.random.normal(0, self.profile['jitter_amplitude'], num_points)
Hand tremor jitter adds realism. Perfect smooth paths indicate robotic movement.
# Occasionally overshoot target then correct
if random.random() < self.profile['overshoot_probability']:
overshoot = random.uniform(5, 15)
x[-1] += overshoot * np.sign(end_x - start_x)
y[-1] += overshoot * np.sign(end_y - start_y)
# Add correction movement
correction_points = 5
corr_x = np.linspace(x[-1], end_x, correction_points)
corr_y = np.linspace(y[-1], end_y, correction_points)
x = np.concatenate([x, corr_x[1:]])
y = np.concatenate([y, corr_y[1:]])
Overshooting and correcting happens naturally in human movement, especially for small targets.
# Generate timestamps with acceleration/deceleration
timestamps = self._generate_timestamps(x, y)
return list(zip(x.astype(int), y.astype(int), timestamps))
def _generate_timestamps(self, x, y):
"""Generate realistic timing with acceleration phases"""
timestamps = [0]
num_points = len(x)
for i in range(1, num_points):
segment_dist = np.sqrt((x[i] - x[i-1])**2 + (y[i] - y[i-1])**2)
base_time = segment_dist / self.profile['avg_speed'] * 1000
# Acceleration phase (start slow)
if i < num_points * 0.2:
base_time *= (1 + self.profile['acceleration'])
# Deceleration phase (slow down for target)
elif i > num_points * 0.8:
base_time *= (1 + self.profile['acceleration'] * 1.5)
# Occasional micro-pauses
if random.random() < self.profile['micro_pause_probability']:
base_time += random.uniform(50, 150)
timestamps.append(timestamps[-1] + base_time)
return [int(t) for t in timestamps]
Acceleration and deceleration phases match Fitts' Law predictions for human movement. NuData's ML models expect this pattern.
Typing Pattern Simulation
class TypingSimulator:
def __init__(self):
self.profile = self._generate_typing_profile()
def _generate_typing_profile(self):
"""Generate unique typing characteristics"""
return {
'avg_dwell_time': np.random.normal(120, 30),
'avg_flight_time': np.random.normal(150, 40),
'backspace_rate': 0.02,
'pause_rate': 0.1,
'common_bigrams': {
'th': 0.8, 'he': 0.85, 'in': 0.82, 'er': 0.83,
'an': 0.84, 'ed': 0.81, 'nd': 0.85, 'to': 0.87,
'on': 0.83, 'es': 0.82, 'or': 0.84, 'ti': 0.81
}
}
Common letter combinations get typed faster because muscle memory takes over. This asymmetry reveals human typists.
def generate_typing_events(self, text):
"""Generate realistic keydown/keyup events with timing"""
events = []
current_time = 0
for i, char in enumerate(text):
# Check for common bigram speed boost
if i > 0:
bigram = text[i-1:i+1].lower()
speed_mult = self.profile['common_bigrams'].get(bigram, 1.0)
else:
speed_mult = 1.0
# Occasional thinking pause
if random.random() < self.profile['pause_rate']:
current_time += np.random.normal(800, 200)
Thinking pauses happen randomly, especially between words or sentences. Perfect consistent timing screams automation.
# Keydown event
events.append({
'type': 'keydown',
'key': char,
'timestamp': current_time
})
# Dwell time (how long key stays pressed)
dwell = np.random.normal(
self.profile['avg_dwell_time'] * speed_mult,
20
)
current_time += max(50, dwell)
# Keyup event
events.append({
'type': 'keyup',
'key': char,
'timestamp': current_time
})
# Flight time to next key
if i < len(text) - 1:
flight = np.random.normal(
self.profile['avg_flight_time'] * speed_mult,
30
)
current_time += max(30, flight)
return events
Both dwell time (key held) and flight time (between keys) vary naturally. Minimum bounds prevent unrealistic sub-30ms delays.
Step 5: Handle Session Management
NuData tracks sessions across requests. Consistent session handling prevents detection.
import pickle
import json
from datetime import datetime, timedelta
import hashlib
class NuDataSessionManager:
def __init__(self, session_file='nudata_sessions.pkl'):
self.session_file = session_file
self.sessions = self._load_sessions()
def _load_sessions(self):
try:
with open(self.session_file, 'rb') as f:
return pickle.load(f)
except FileNotFoundError:
return {}
def _save_sessions(self):
with open(self.session_file, 'wb') as f:
pickle.dump(self.sessions, f)
Persisting sessions to disk enables resuming automation across script restarts without triggering "new device" flags.
def create_session(self, site_url, fingerprint_profile=None):
"""Create new NuData session with consistent fingerprint"""
session_id = hashlib.md5(
f"{site_url}{time.time()}{random.random()}".encode()
).hexdigest()
# Generate consistent behavioral profile for this session
behavioral_profile = fingerprint_profile or {
'typing_speed': random.gauss(120, 20),
'mouse_speed': random.gauss(400, 80),
'scroll_pattern': random.choice(['smooth', 'jumpy', 'mixed']),
'interaction_delays': {
'min': random.uniform(0.5, 1.0),
'max': random.uniform(2.0, 4.0),
'avg': random.uniform(1.2, 2.0)
},
'screen_resolution': random.choice([
(1920, 1080), (1440, 900), (2560, 1440), (1366, 768)
]),
'timezone_offset': random.choice([-8, -7, -6, -5, -4, 0, 1, 2])
}
Behavioral profiles stay consistent within sessions but vary between them. Profile drift within a session triggers anomaly detection.
self.sessions[session_id] = {
'id': session_id,
'site': site_url,
'created': datetime.now(),
'last_used': datetime.now(),
'cookies': {},
'tokens': [],
'behavioral_profile': behavioral_profile,
'request_count': 0,
'device_fingerprint': self._generate_device_fingerprint()
}
self._save_sessions()
return session_id
def _generate_device_fingerprint(self):
"""Generate consistent device fingerprint for session"""
platforms = [
{'os': 'Windows', 'platform': 'Win32', 'arch': 'x64'},
{'os': 'macOS', 'platform': 'MacIntel', 'arch': 'x64'},
{'os': 'Linux', 'platform': 'Linux x86_64', 'arch': 'x64'}
]
chosen = random.choice(platforms)
return {
**chosen,
'cores': random.choice([4, 6, 8, 12, 16]),
'memory': random.choice([4, 8, 16, 32]),
'language': 'en-US',
'languages': ['en-US', 'en'],
'webgl_vendor': 'Google Inc. (Intel)',
'webgl_renderer': 'ANGLE (Intel, Intel(R) UHD Graphics Direct3D11)'
}
Device fingerprints must remain consistent throughout a session. Changing WebGL renderer mid-session is impossible in reality.
def get_valid_session(self, site_url, max_age_hours=4):
"""Get existing valid session or create new one"""
cutoff = datetime.now() - timedelta(hours=max_age_hours)
for session_id, session in self.sessions.items():
if (session['site'] == site_url and
session['last_used'] > cutoff and
session['request_count'] < 100): # Rate limit per session
session['last_used'] = datetime.now()
self._save_sessions()
return session_id
return self.create_session(site_url)
def update_session(self, session_id, token=None, cookies=None):
"""Update session with new token or cookies"""
if session_id not in self.sessions:
return
session = self.sessions[session_id]
session['last_used'] = datetime.now()
session['request_count'] += 1
if token:
session['tokens'].append({
'token': token,
'timestamp': datetime.now()
})
session['tokens'] = session['tokens'][-10:] # Keep recent only
if cookies:
session['cookies'].update(cookies)
self._save_sessions()
Request counts per session prevent overuse. Real users don't make 1000 requests from one device in an hour.
Step 6: Evade the Trust Consortium
The Trust Consortium shares threat intelligence across NuData's 4.5 billion monitored devices. Flagged fingerprints propagate to all clients.
Fingerprint Rotation Strategy
import hashlib
import random
import time
class FingerprintRotator:
def __init__(self):
self.fingerprint_pools = self._build_pools()
self.used_combinations = set()
def _build_pools(self):
"""Build pools of realistic fingerprint components"""
return {
'screens': [
{'width': 1920, 'height': 1080, 'depth': 24, 'ratio': 1},
{'width': 2560, 'height': 1440, 'depth': 24, 'ratio': 1},
{'width': 1440, 'height': 900, 'depth': 24, 'ratio': 2},
{'width': 1366, 'height': 768, 'depth': 24, 'ratio': 1},
{'width': 3840, 'height': 2160, 'depth': 24, 'ratio': 2}
],
'webgl_renderers': [
('Google Inc. (Intel)', 'ANGLE (Intel, Intel(R) UHD Graphics Direct3D11)'),
('Google Inc. (NVIDIA)', 'ANGLE (NVIDIA, NVIDIA GeForce RTX 3060 Direct3D11)'),
('Google Inc. (AMD)', 'ANGLE (AMD, AMD Radeon RX 6700 XT Direct3D11)'),
('Google Inc. (Intel)', 'ANGLE (Intel, Intel(R) Iris(R) Xe Graphics Direct3D11)'),
('Apple Inc.', 'Apple M1 Pro')
],
'audio_contexts': [
35.10893232, 35.74996031, 35.73833847,
35.10851453, 35.73996812
],
'canvas_noises': [
0.00001, 0.00002, 0.00003, 0.00004, 0.00005
]
}
Real devices produce consistent fingerprints, but the pool contains actual values from real browsers.
def generate_fresh_fingerprint(self):
"""Generate fingerprint combination not previously used"""
max_attempts = 100
for _ in range(max_attempts):
fp = {
'screen': random.choice(self.fingerprint_pools['screens']),
'webgl': random.choice(self.fingerprint_pools['webgl_renderers']),
'audio': random.choice(self.fingerprint_pools['audio_contexts']),
'canvas_noise': random.choice(self.fingerprint_pools['canvas_noises'])
}
# Create hash for deduplication
fp_hash = hashlib.md5(str(fp).encode()).hexdigest()
if fp_hash not in self.used_combinations:
self.used_combinations.add(fp_hash)
return fp
# If pool exhausted, clear and start fresh
self.used_combinations.clear()
return self.generate_fresh_fingerprint()
Tracking used combinations prevents reusing flagged fingerprints. After exhausting the pool, clearing enables reuse with fresh IP/session contexts.
def validate_consistency(self, fingerprint):
"""
Ensure fingerprint components are consistent with each other.
Mismatches between screen size and WebGL capabilities trigger flags.
"""
screen = fingerprint['screen']
webgl_vendor, webgl_renderer = fingerprint['webgl']
# High-res screens shouldn't pair with weak GPUs
if screen['width'] >= 2560:
weak_gpus = ['Intel(R) UHD Graphics', 'Intel(R) HD Graphics']
if any(gpu in webgl_renderer for gpu in weak_gpus):
return False
# Mac GPUs only with Mac screens typically
if 'Apple' in webgl_vendor:
# Apple devices have specific resolution patterns
valid_mac_res = [
(1440, 900), (2560, 1600), (2880, 1800), (3024, 1964)
]
if (screen['width'], screen['height']) not in valid_mac_res:
return False
return True
Consistency validation prevents impossible hardware combinations. A 4K screen with Intel HD Graphics 3000 doesn't exist.
IP Reputation Management
class IPReputationManager:
def __init__(self, proxy_list):
self.proxies = proxy_list
self.ip_scores = {}
self.blacklist = set()
def get_clean_proxy(self, site_domain):
"""Get proxy with good reputation for target site"""
available = [
p for p in self.proxies
if p not in self.blacklist and
self._get_score(p, site_domain) > 0.5
]
if not available:
# Reset blacklist if exhausted
self.blacklist.clear()
available = self.proxies
return random.choice(available)
Residential proxies from Roundproxies have better reputation than datacenter IPs. Their residential and ISP proxies appear as legitimate home connections.
def _get_score(self, proxy, domain):
"""Calculate reputation score for proxy/domain combo"""
key = f"{proxy}:{domain}"
if key not in self.ip_scores:
self.ip_scores[key] = 1.0 # New proxy starts clean
return self.ip_scores[key]
def report_result(self, proxy, domain, success):
"""Update reputation based on request result"""
key = f"{proxy}:{domain}"
current = self.ip_scores.get(key, 1.0)
if success:
self.ip_scores[key] = min(1.0, current + 0.1)
else:
self.ip_scores[key] = max(0.0, current - 0.3)
if self.ip_scores[key] < 0.2:
self.blacklist.add(proxy)
Reputation scoring adapts to changing conditions. Failed requests more heavily penalize IPs than successes reward them.
Step 7: Advanced Fingerprint Spoofing
Canvas and WebGL fingerprinting require hardware-level spoofing for proper bypass.
Canvas Fingerprint Manipulation
def inject_canvas_spoofing(page):
"""
Inject canvas spoofing that adds consistent noise.
The noise must be deterministic per session but unique overall.
"""
spoof_script = """
(function() {
// Generate session-specific noise seed
const noiseSeed = Math.random() * 1000;
// Override toDataURL
const originalToDataURL = HTMLCanvasElement.prototype.toDataURL;
HTMLCanvasElement.prototype.toDataURL = function(type) {
if (type === 'image/png' || type === 'image/jpeg') {
const context = this.getContext('2d');
if (context) {
const imageData = context.getImageData(
0, 0, this.width, this.height
);
// Add deterministic noise based on seed
for (let i = 0; i < imageData.data.length; i += 4) {
const noise = Math.sin(noiseSeed + i) * 0.5;
imageData.data[i] += noise;
imageData.data[i + 1] += noise;
imageData.data[i + 2] += noise;
}
context.putImageData(imageData, 0, 0);
}
}
return originalToDataURL.apply(this, arguments);
};
})();
"""
return spoof_script
Deterministic noise ensures the canvas hash stays consistent within a session while differing from the true hardware fingerprint.
WebGL Spoofing
def inject_webgl_spoofing(page, vendor, renderer):
"""
Spoof WebGL vendor and renderer strings.
These must match your claimed platform.
"""
spoof_script = f"""
(function() {{
const getParameter = WebGLRenderingContext.prototype.getParameter;
WebGLRenderingContext.prototype.getParameter = function(param) {{
// UNMASKED_VENDOR_WEBGL
if (param === 37445) {{
return '{vendor}';
}}
// UNMASKED_RENDERER_WEBGL
if (param === 37446) {{
return '{renderer}';
}}
return getParameter.apply(this, arguments);
}};
// Also handle WebGL2
if (typeof WebGL2RenderingContext !== 'undefined') {{
const getParameter2 = WebGL2RenderingContext.prototype.getParameter;
WebGL2RenderingContext.prototype.getParameter = function(param) {{
if (param === 37445) return '{vendor}';
if (param === 37446) return '{renderer}';
return getParameter2.apply(this, arguments);
}};
}}
}})();
"""
return spoof_script
WebGL spoofing at the API level beats extension-based solutions that anti-bots detect through other means.
Audio Context Spoofing
def inject_audio_spoofing(page, target_hash):
"""
Spoof AudioContext fingerprint.
Audio fingerprints vary by hardware and are hard to fake properly.
"""
spoof_script = f"""
(function() {{
const AudioContext = window.AudioContext || window.webkitAudioContext;
if (AudioContext) {{
const originalCreateOscillator = AudioContext.prototype.createOscillator;
const originalCreateDynamicsCompressor = AudioContext.prototype.createDynamicsCompressor;
AudioContext.prototype.createOscillator = function() {{
const osc = originalCreateOscillator.apply(this, arguments);
// Add slight frequency offset
const originalFrequency = osc.frequency;
osc.frequency.value = originalFrequency.value + (Math.random() - 0.5) * 0.001;
return osc;
}};
AudioContext.prototype.createDynamicsCompressor = function() {{
const comp = originalCreateDynamicsCompressor.apply(this, arguments);
// Modify threshold slightly
comp.threshold.value = comp.threshold.value + (Math.random() - 0.5) * 0.0001;
return comp;
}};
}}
}})();
"""
return spoof_script
Audio fingerprinting uses oscillator and compressor characteristics. Minor modifications create unique hashes without breaking functionality.
Common Pitfalls and How to Avoid Them
Pitfall 1: Inconsistent Fingerprints
Mismatched fingerprint components trigger instant detection:
# BAD: Windows UA with Mac platform
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0...) Chrome/131',
'Sec-CH-UA-Platform': '"macOS"' # Mismatch!
}
# GOOD: Consistent fingerprint
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0...) Chrome/131',
'Sec-CH-UA-Platform': '"Windows"'
}
Pitfall 2: Robotic Timing
Perfect timing patterns expose automation:
# BAD: Fixed 1-second delays
for item in items:
click_element(item)
time.sleep(1.0) # Exactly 1 second every time
# GOOD: Variable human-like timing
for item in items:
click_element(item)
time.sleep(random.gauss(1.0, 0.2)) # Varies around 1 second
Pitfall 3: Missing Behavioral Noise
Real users make mistakes:
# BAD: Perfect typing
def type_text(text):
for char in text:
type_key(char)
time.sleep(0.1)
# GOOD: Typing with occasional errors
def type_text_human(text):
for char in text:
if random.random() < 0.02:
type_key(random.choice('aeiou')) # Typo
time.sleep(0.3)
type_key('backspace') # Correction
type_key(char)
time.sleep(random.gauss(0.1, 0.03))
Pitfall 4: Ignoring Rate Limits
Even perfect bypass fails against rate limits:
class RateLimiter:
def __init__(self, requests_per_minute=30):
self.min_delay = 60.0 / requests_per_minute
self.last_request = 0
def wait(self):
elapsed = time.time() - self.last_request
if elapsed < self.min_delay:
sleep_time = self.min_delay - elapsed + random.uniform(0, 2)
time.sleep(sleep_time)
self.last_request = time.time()
Pitfall 5: Static Fingerprints Across Sessions
Never reuse exact fingerprints:
# BAD: Same fingerprint every session
FINGERPRINT = generate_fingerprint()
for session in sessions:
use_fingerprint(FINGERPRINT)
# GOOD: Fresh fingerprint per session
for session in sessions:
fingerprint = generate_fresh_fingerprint()
use_fingerprint(fingerprint)
Hidden Tricks and Pro Tips
Trick 1: Pre-warm Sessions
NuData scores new sessions with higher suspicion. Pre-warm before critical actions:
async def prewarm_session(browser, target_url):
"""
Visit non-critical pages first to build behavioral history.
This establishes trust before sensitive actions.
"""
warmup_urls = [
f"{target_url}/about",
f"{target_url}/help",
f"{target_url}/faq"
]
for url in random.sample(warmup_urls, 2):
page = await browser.get(url)
await page.scroll_down(random.randint(100, 300))
await page.sleep(random.uniform(5, 15))
# Now session has history
return await browser.get(target_url)
Trick 2: Exploit Fingerprint Caching
Sites cache fingerprint results. Trigger recalculation after establishing good behavior:
async def trigger_fingerprint_refresh(page):
"""
Force fingerprint recalculation after behavioral warmup.
Some implementations cache initial (suspicious) score.
"""
# Clear canvas and WebGL contexts
await page.evaluate("""
() => {
document.querySelectorAll('canvas').forEach(c => {
const ctx = c.getContext('2d');
if (ctx) ctx.clearRect(0, 0, c.width, c.height);
});
}
""")
# Trigger new fingerprint calculation
await page.evaluate("""
() => {
// Force NuData to recollect
if (window.nds && window.nds.refresh) {
window.nds.refresh();
}
}
""")
Trick 3: Time-Based Behavioral Patterns
Match activity to expected human patterns:
def get_activity_delay():
"""
Return delay based on time of day.
Night activity should be slower (tired users).
"""
hour = datetime.now().hour
if 0 <= hour < 6: # Late night
return random.uniform(3, 8)
elif 6 <= hour < 9: # Morning rush
return random.uniform(1, 3)
elif 9 <= hour < 17: # Work hours
return random.uniform(1.5, 4)
elif 17 <= hour < 22: # Evening leisure
return random.uniform(2, 5)
else: # Late evening
return random.uniform(2.5, 6)
Trick 4: Geographic Consistency
IP location must match timezone and language settings:
def validate_geo_consistency(proxy_location, fingerprint):
"""
Ensure fingerprint matches proxy geography.
US IP with French language triggers flags.
"""
location_profiles = {
'US': {
'timezones': [-10, -9, -8, -7, -6, -5, -4],
'languages': ['en-US', 'en'],
'date_format': 'MM/DD/YYYY'
},
'UK': {
'timezones': [0, 1],
'languages': ['en-GB', 'en'],
'date_format': 'DD/MM/YYYY'
},
'DE': {
'timezones': [1, 2],
'languages': ['de-DE', 'de', 'en'],
'date_format': 'DD.MM.YYYY'
}
}
expected = location_profiles.get(proxy_location)
if not expected:
return True # Unknown location, skip validation
if fingerprint['timezone'] not in expected['timezones']:
return False
if fingerprint['language'] not in expected['languages']:
return False
return True
Trick 5: Token Timestamp Manipulation
Tokens contain timestamps. Stale tokens get rejected:
def refresh_token_timestamp(token):
"""
Update token timestamp to current time.
NuData rejects tokens older than ~30 seconds.
"""
try:
decoded = base64.b64decode(token).decode()
data = json.loads(decoded)
# Update timestamp fields
current_ms = int(time.time() * 1000)
data['timestamp'] = current_ms
data['wkr'] = current_ms # Session duration
return base64.b64encode(json.dumps(data).encode()).decode()
except Exception:
return token # Return original if parsing fails
Final Thoughts
Bypassing NuData in 2026 requires understanding their multi-layered detection approach. No single technique works—you need proper token generation, realistic behavioral simulation, consistent fingerprinting, and smart session management working together.
The key insight: NuData doesn't look for perfect human behavior. It looks for inhuman patterns. Slight imperfections like typos, hesitation, and variable timing actually help your bypass succeed.
Start with Nodriver for the best balance of stealth and usability. Add behavioral simulation gradually. Use residential proxies from reputable providers to avoid IP reputation issues. Test against detection tools like deviceandbrowserinfo.com before targeting production sites.
Remember that these techniques work for legitimate automation testing and research purposes. Always respect terms of service and legal requirements for your jurisdiction.
FAQ
What is the nds-pmd token?
The nds-pmd token contains encoded behavioral data that NuData analyzes. It includes mouse movements, typing patterns, scroll events, device fingerprints, and session information. Without a valid token, requests to NuData-protected sites fail immediately. The token must be generated with realistic behavioral data and submitted within approximately 30 seconds of creation.
Why does Puppeteer Stealth no longer work?
Puppeteer Stealth patches JavaScript APIs but doesn't address CDP protocol detection. Modern anti-bots detect the Chrome DevTools Protocol commands that automation tools send. Nodriver and SeleniumBase CDP mode avoid this by minimizing or eliminating detectable CDP usage.
How often should I rotate fingerprints?
Rotate fingerprints between sessions, not during them. A single session should maintain consistent fingerprints—real devices don't change hardware mid-browsing. Create fresh fingerprints for each new session while ensuring component consistency (matching GPU capabilities to screen resolution).
Can NuData detect residential proxies?
NuData can flag residential proxies through behavioral analysis even if IP reputation is clean. The Trust Consortium shares device fingerprints across clients, so a flagged fingerprint on one site affects all sites using NuData. Residential proxies from providers like Roundproxies help but aren't sufficient alone—proper behavioral simulation remains essential.
What's the best browser for NuData bypass?
Nodriver currently offers the best balance of stealth and usability for NuData bypass. It avoids CDP detection while providing Pythonic automation capabilities. For cases requiring maximum stealth, SeleniumBase's CDP mode with PyAutoGUI input simulation approaches real human input at the OS level.