An interactive browser-based tool for running and analyzing browser fingerprinting and automation/bot detection checks. Built on top of the VexTrio fingerprinting script documented in the original research below, extended with modern detection techniques from 2022–2025.
All checks in the a-series (a1–a92) are derived from the VexTrio threat actor's fingerprinting script, originally documented by gi7w0rm:
gi7w0rm — VexTrio's Browser Fingerprinting https://gi7w0rm.medium.com/vextrios-browser-fingerprinting-aeb721be6e30
VexTrio is a large-scale traffic distribution system (TDS) used to monetize stolen traffic by routing victims to scam, phishing, and malware pages. Their fingerprinting script ran silently on compromised sites to determine whether a visitor was a real human or a security researcher/bot before redirecting.
Open Fingerprint.html in a browser. Click Run All Checks. Each check runs synchronously against the current browser environment and returns one of:
| Result | Meaning |
|---|---|
aNN:0 / bNN:0 |
PASS — no anomaly detected |
aNN:1 / bNN:1 |
FAIL — browser flagged as suspicious/automated |
aNN:e / bNN:e |
ERROR — check threw an exception |
The verdict a0:1 means the browser was flagged as suspicious by at least one check.
The Code Generator panel lets you export any subset of checks as a deployable JavaScript snippet in three styles (chained if-else, loop, standalone result map).
| ID | Name | Detection Method |
|---|---|---|
| a1 | Language ISO Code Mismatch | navigator.languages[0] ISO vs navigator.language ISO |
| a2 | Window vs Screen Size | screen.width/height vs screen.availWidth/Height |
| a3 | OSCPU vs UserAgent OS | navigator.oscpu (Firefox-only) vs UA OS string |
| a4 | OSCPU on Non-Firefox | navigator.oscpu defined on non-Firefox = spoofed |
| a5 | Platform vs UserAgent | navigator.platform cross-referenced with UA OS |
| a6 | Plugins Undefined on Non-Windows | navigator.plugins undefined on non-Windows |
| a7 | Browser Build Number | navigator.productSub must be "20030107" for Chrome/Safari |
| a8 | eval.toString() Length | eval.toString().length varies: 33=Chromium, 39=IE, 37=Safari iOS |
| a9 | toSource() Function | Obsolete Mozilla method only in old Firefox |
| a10 | WebGL Vendor vs OS/Browser | UNMASKED_VENDOR_WEBGL cross-checked against UA OS and browser type |
| a11 | WebDriver Detection | navigator.webdriver === true |
| a12 | Permission State Anomaly | Notification.permission === "denied" + state === "prompt" |
| a13 | Permissions API Integrity | navigator.permissions.query.toString() must be native code |
| a14 | DevTools Console Detection | Chrome: regex .toString() call count in console.debug() |
| a15 | PhantomJS Detection | callPhantom, _phantom, phantom in window |
| a16 | Browser Automation DOM | Selenium/WebDriver/Nightmare/PhantomJS DOM properties |
| a17 | Phantomas Detection | __phantomas in window |
| a18 | Selenium DOM Cache | $[a-z]dc_ pattern with .cache_ in document properties |
| a19 | Node.js Buffer | window.Buffer !== undefined |
| a20 | Chromium Automation Driver | window.domAutomation / window.domAutomationController |
| a21 | setTimeout Integrity | setTimeout.toString() must match native code |
| a22 | setInterval Integrity | setInterval.toString() must match native code |
| a42 | XHR Open Integrity | XMLHttpRequest.prototype.open.toString() — checks for klIsCORSRequest |
| a43 | XHR Send Integrity | XMLHttpRequest.prototype.send.toString() — same |
| a60 | HeadlessChrome UA String | UA contains phantomjs, headless, avira, googleweblight |
| a78 | Stack Trace Behavior | Stack trace matches Puppeteer-stealth Object.apply(<anonymous>) regex |
| a86 | iOS Logical Processors | iOS UA with hardwareConcurrency > 4 (Safari dropped API support) |
| a89 | Browser Voice List | "Lekha" voice (Apple-only) present on Windows/Android/Linux |
| a92 | VirtualBox WebGL | WebGL renderer contains "VirtualBox" |
These checks were added based on modern research into headless browser detection, stealth evasion bypass analysis, and browser inconsistency research.
| ID | Name | Detection Method | Source / Technique |
|---|---|---|---|
| b1 | window.chrome Object Integrity | Chrome UA must have window.chrome.loadTimes / csi / runtime / app; automation often exposes empty {} |
Puppeteer-stealth chrome.runtime evasion; Antoine Vastel's headless Chrome research |
| b2 | ChromeDriver cdc_ Property Injection | ChromeDriver injects window.$cdc_adoQpoasnfa76pfcZLmcfl_* properties detectable via Object.getOwnPropertyNames |
ChromeDriver source; Berstend/puppeteer-extra stealth evasions |
| b3 | UA-CH Platform vs UserAgent | navigator.userAgentData.platform must match OS from UA string; also mobile flag must match |
UA-CH (User-Agent Client Hints) inconsistency research; FingerprintJS Pro signals |
| b4 | Intl Timezone vs Date Offset | Intl.DateTimeFormat().resolvedOptions().timeZone computed offset must agree with Date.getTimezoneOffset() within 90 min |
Timezone spoofing detection; BrowserLeaks.com, CreepJS |
| b5 | navigator.vendor vs Browser | Chrome="Google Inc.", Safari="Apple Computer, Inc.", Firefox="" |
Well-known UA inconsistency; FingerprintJS |
| b6 | iOS devicePixelRatio | All iOS devices since iPhone 4 (2010) have devicePixelRatio >= 2 (Retina) |
Hardware consistency check; nstbrowser/anti-detect research |
| b7 | Touch Points on Mobile OS | iOS/Android UA must have navigator.maxTouchPoints > 0 |
BrowserLeaks touch detection; headless browser emulation checks |
| b8 | Error.stackTraceLimit | V8/Chrome-only property; present in Chrome/Edge, absent in Firefox/Safari | V8 engine fingerprinting; CreepJS engine checks |
| b9 | Prototype Lie Detection | Checks Function.prototype.toString.call(toString) is native; detects Proxy wrappers on navigator.webdriver, plugins, languages, userAgent getters |
CreepJS queryLies() technique; puppeteer-extra-plugin-stealth evasion analysis |
| b10 | Canvas Rendering Integrity | canvas.toDataURL() must produce non-empty output (>200 bytes); "data:," = canvas blocked |
Headless canvas stripping detection; FingerprintJS canvas check |
| b11 | AudioContext Sample Rate | AudioContext.sampleRate must be a standard rate (44100/48000/etc.); abnormal = synthetic env |
AudioContext fingerprinting; CreepJS audio fingerprint |
| b12 | Network Information API | Chrome on Android must expose navigator.connection (NetworkInformation API) |
Chrome Android API surface check |
| b13 | Playwright Window Artifacts | Checks for __playwright, __pw_manual, __pw_preview, __pwInitScripts in window |
Playwright source code inspection; playwright-stealth bypass research |
| b14 | Notification API Presence | Desktop Chrome/Firefox/Edge must have Notification constructor; absent = stripped automation env |
API surface completeness check; headless stripping research |
| b15 | Math IEEE 754 Consistency | Math.tan(-1e308), Math.acos(1.0001), Math.log(-1), 1/-0 must return standard IEEE 754 values |
VM/non-standard JS engine detection; CreepJS math fingerprint |
| b16 | document.hasFocus() | Headless Chrome returns false; real user sessions are focused |
Headless Chrome behavioral difference; Antoine Vastel 2023 research |
| b17 | UA-CH Brands Decoy Entry | Chromium's navigator.userAgentData.brands always includes a "Not_A Brand" / "Not;A Brand" decoy; absent = spoofed brands |
Chromium UA-CH spec implementation; anti-spoofing check |
| b18 | Media Devices API | Desktop Chrome/Firefox/Edge must expose navigator.mediaDevices.enumerateDevices |
API surface completeness; headless stripping |
| b19 | Brave Browser Detection | navigator.brave.isBrave() present while UA claims Chrome = Brave masquerading |
Brave browser fingerprinting; unmasked API exposure |
| b20 | WebRTC Presence | RTCPeerConnection must exist in modern Chrome/Firefox/Edge/Safari; absent = stripped env |
API surface completeness; headless/privacy proxy detection |
| b21 | Outer Window Dimensions | window.outerWidth/Height === 0 = old headless Chrome (pre-112) |
Headless behavioral difference; Chrome 112 headless unification |
| b22 | Connection RTT Zero | navigator.connection.rtt === 0 = headless; real browsers report non-zero RTT |
Network Information API headless signal; infosimples/detect-headless |
| b23 | PDF Viewer Enabled | Chrome 105+ must have pdfViewerEnabled=true; false = stripped Chromium build |
MDN navigator.pdfViewerEnabled; headless stripping research |
| b24 | MimeTypeArray Prototype | Object.prototype.toString.call(navigator.mimeTypes) must be "[object MimeTypeArray]" |
Native prototype integrity check |
| b25 | Image Constructor Integrity | new Image() instanceof HTMLImageElement must be true |
DOM prototype integrity; headless API stripping |
| b26 | Media Codec Chromium vs Chrome | canPlayType('audio/mp4; codecs="mp4a.40.2"') returns "probably" on real Chrome, "maybe" on Chromium |
Licensed codec difference; privacycheck.sec.lrz.de; deviceandbrowserinfo.com |
| b27 | Google TTS Voice Count | Chrome UA with >10 voices but 0 starting with "Google " = open-source Chromium binary |
deviceandbrowserinfo.com headless Chrome fingerprint 2024 |
| b28 | deviceMemory Valid Set | navigator.deviceMemory must be in {0.25, 0.5, 1, 2, 4, 8}; any other value = spoofed |
Castle.io deviceMemory bot detection deep dive |
| b29 | iframe webdriver Leak | Creates iframe and checks iframe.contentWindow.navigator.webdriver; stealth patches often miss iframes |
Berstend/puppeteer-extra-plugin-stealth iframe.contentWindow evasion |
| b30 | UA-CH "Google Chrome" Brand | Chrome UA must include "Google Chrome" in navigator.userAgentData.brands; Chromium builds lack this |
Chromium UA-CH spec; deviceandbrowserinfo.com 2024 |
| b31 | macOS screen.availTop | macOS always reserves ≥25px for the menu bar (screen.availTop > 0); headless Chrome defaults to 0 |
Chromium headless screen_info README; Chromium headless/screen_info/README.md |
| b32 | Network Downlink Zero | navigator.connection.downlink === 0 = headless; real browsers report positive Mbps |
Network Information API; infosimples/detect-headless; WICG netinfo spec |
| b33 | ChromeDriver webdriverAsyncExecutor | window.__$webdriverAsyncExecutor presence; extended prototype-chain scan for cdc_*_Array/Promise/Symbol/Proxy/Object variants |
CreepJS issue #182; brotector.js; ultrafunkamsterdam/undetected-chromedriver |
| b34 | chrome.loadTimes First Paint | chrome.loadTimes().firstPaintAfterLoadTime === 0 in headless = no GPU render pipeline |
Chrome loadTimes deprecation docs; Castle.io headless Chrome research |
- gi7w0rm — VexTrio's Browser Fingerprinting (2024): https://gi7w0rm.medium.com/vextrios-browser-fingerprinting-aeb721be6e30
- Antoine Vastel — New Headless Chrome and Browser Fingerprint (2023): https://antoinevastel.com/bot%20detection/2023/02/19/new-headless-chrome.html
- Antoine Vastel — Detecting Chrome Headless blog series: https://antoinevastel.com/
- Berstend / puppeteer-extra-plugin-stealth source (evasion list = detection surface): https://github.com/berstend/puppeteer-extra/tree/master/packages/puppeteer-extra-plugin-stealth/evasions
- CreepJS — Comprehensive client-side fingerprinting with prototype lie detection: https://github.com/abrahamjuliot/creepjs
- FingerprintJS — Open-source browser fingerprinting library: https://github.com/fingerprintjs/fingerprintjs
- BrowserLeaks — Browser leak and fingerprint testing: https://browserleaks.com
- Cover Your Tracks (EFF) — https://coveryourtracks.eff.org
- ChromeDriver
cdc_injection: https://source.chromium.org/chromium/chromium/src/+/main:chrome/test/chromedriver/ - Automation controlled flag:
--disable-blink-features=AutomationControlled
- MDN NavigatorUAData: https://developer.mozilla.org/en-US/docs/Web/API/NavigatorUAData
- Chromium UA-CH spec: https://wicg.github.io/ua-client-hints/
The tool was validated against Playwright headless Chromium across three scenarios. All new b-series checks were verified:
Tested against Playwright 1.58.2 + Chromium on macOS. Tool: 63 total checks.
PASS: 51 / 63 FAIL: 12
Flagged: a11 (webdriver=true), a12 (permission anomaly), a60 (HeadlessChrome UA),
b1 (window.chrome missing), b11 (sampleRate=24000), b18 (no mediaDevices),
b22 (rtt=0), b23 (pdfViewerEnabled=false), b27 (0 Google TTS voices),
b28 (deviceMemory undefined), b29 (iframe webdriver leak), b31 (screen.availTop=0)
PASS: 53 / 63 FAIL: 10
Bypassed: a11 (webdriver), b29 (iframe leak). Still flagged: a12, a60, b1, b11, b18, b22, b23, b27, b28, b31
PASS: 54 / 63 FAIL: 9
Bypassed: a11, a60, b29. Still flagged: a12, b1, b11, b18, b22, b23, b27, b28, b31
Key findings:
- Even with full stealth UA spoofing, 9 checks still flag Playwright headless Chromium
- The most persistent signals are Chromium-binary vs real Chrome differences:
window.chromemissing (b1), AudioContext sampleRate=24000Hz (b11), nomediaDevices(b18), RTT=0 (b22),pdfViewerEnabled=false(b23), 0 Google TTS voices (b27),deviceMemoryundefined (b28) - b31 fires in all scenarios: Playwright's headless Chromium on macOS reports
screen.availTop=0because there is no OS menu bar in the headless window session - Playwright headless Chrome returns
AudioContext.sampleRate = 24000(not in the standard 44100/48000 set used by real browsers) - b27 (0 Google TTS voices with 191 total) is highly reliable: Playwright uses the open-source Chromium binary which ships without Google's proprietary TTS engine
- b26 (codec canPlayType check) does not fire on macOS — Playwright's bundled Chromium includes FFmpeg and returns "probably" for AAC/H.264; this check is more reliable on Linux CI environments
- b32 (downlink=0), b33 (cdc_ async executor), b34 (loadTimes firstPaint) do not fire against Playwright: b32 because Playwright Chromium exposes a non-zero downlink value; b33 because
__$webdriverAsyncExecutoris ChromeDriver-specific; b34 becausewindow.chromeis absent in headless (b1 fires first)
- Open
Fingerprint.htmlin a browser (no server required — static HTML) - Click Run All Checks
- Review per-check results. Red = flagged, Green = clean, Yellow = error
- Use the Code Generator to export checks as embeddable JS
The Code Generator supports three output styles:
- Chained — short-circuit
if/else ifchain (matches original VexTrio style) - Loop — iterates an array of check functions
- Standalone — returns
{verdict, checks}map for full per-check logging
For research and educational purposes. Techniques documented here are used in the wild by threat actors and security vendors alike.