Besides measuring the device, WhiskerJS looks for traces of robots (browser automation tools) and of scripts that fake browser values. Each check that sees something adds a flag to the result. A flag is a hint, not a verdict: privacy extensions and normal setups can trigger some of them. Your server decides what a flag means for you.
result.integrityFlags is a sorted list of the checks that fired. Each one
is a sign that the page may be driven by automation or that browser
functions were changed. None of them is proof, and WhiskerJS never turns
them into a verdict: that is your server’s decision, ideally together with
other signals you have (account history, rate, network).
How the checks work, in short:
- they run after the collectors, read only, and never write to the page;
- a check that throws simply does not fire;
- two checks (
iframe_mismatch,prototype_lie) compare against a fresh, hiddenabout:blankiframe that is removed straight away (see How it works); they are skipped after an abort; - the names are exported as
INTEGRITY_FLAG_NAMES.
The flags
automation_globals
- Fires when: the window has a global left by an automation tool (32
known names from Selenium, WebDriver, Watir, Nightmare, Playwright,
PhantomJS, CEF, CefSharp, Awesomium, FMiner, Geb, Phantomas, WebdriverIO
and older drivers), a
documentproperty starts with$cdc_,$wdc_orcdc_(ChromeDriver), or<html>has awebdriver,seleniumordriverattribute. - False positives: a page script or extension that happens to define a
global with one of these names (for example
geborawesomium). Rare.
automation_webdriver
- Fires when:
navigator.webdriveristrue. - Meaning: the browser itself says it is controlled through WebDriver or a similar protocol. Selenium, Playwright and Puppeteer set it by default.
- False positives: none in the strict sense, but your own end-to-end tests, accessibility scanners and monitoring robots set it too.
engine_mismatch
- Fires when: the browser class says one engine but the code behaves
like another: the printed source of
evalis 33 characters in V8 and 37 in Gecko and WebKit, andnavigator.productSubis20100101in Gecko and20030107elsewhere. Not checked for classother. - Meaning: the user agent was probably changed to claim another browser.
- False positives: user-agent switcher extensions used by developers, and uncommon browsers whose user agent names another engine.
headless_markers
- Fires when:
outerWidthandouterHeightare both 0, or the user agent is desktop Chrome butwindow.chromeis missing. - False positives: some embedded Chromium views and webviews that send a
desktop Chrome user agent without
window.chrome.
headless_user_agent
- Fires when: the user agent contains
HeadlessChrome,PhantomJSorSlimerJS. - False positives: none expected; trivial to hide, so its absence means little.
iframe_mismatch
- Fires when: any of these differ between the page and the fresh
iframe:
navigator.userAgent,platform,vendor,language,languages,hardwareConcurrency,deviceMemory,webdriver,screen.width,screen.height,screen.colorDepth, or the Intl time zone. - Meaning: something overrode these values in the page but not in new
frames, a common trait of spoofing scripts. A spoof applied the same way
to both is not a mismatch (and may be caught by
prototype_lie). - False positives: it has been seen under WebKit automation (for example Playwright’s WebKit build), where the automation layer itself makes the page and a new frame disagree. The exact field depends on the setup; check which value differs before acting on it. Also possible with extensions that inject only into the top frame.
impossible_hardware
- Fires when:
hardwareConcurrencyis a number but not a whole number from 1 to 256, ordeviceMemoryis a number but not a power of two from 0.25 to 64. - Meaning: the values were set by hand.
- False positives: none expected for current browsers.
native_tampering
- Fires when: one of 23 built-in functions or getters does not look
native: its source is not
function name() { [native code] }, the printed name differs from itsnameproperty, or it has extra own properties. Checked:Function.prototype.toStringfirst (patching it would hide everything else), then canvas (toDataURL,getContext,getImageData,fillText,measureText), WebGL (getParameter,readPixels),AudioBuffer.getChannelData,Date.getTimezoneOffset,Element.getBoundingClientRect,Intl.DateTimeFormat.resolvedOptions, the navigator gettersuserAgent,platform,languages,hardwareConcurrency,deviceMemory,webdriver,plugins, the screen getterswidth,height,colorDepth, andHTMLElement.offsetWidth. - Meaning: a script replaced a function that fingerprinting reads.
- False positives: privacy and anti-fingerprinting extensions do exactly this on purpose, and so can some monitoring or polyfill scripts on your own page. Expect it on privacy-minded users’ browsers.
navigator_own_property
- Fires when: one of
deviceMemory,hardwareConcurrency,language,languages,platform,plugins,userAgent,vendororwebdriveris defined on thenavigatorobject itself instead of its prototype. - Meaning: overridden with
Object.defineProperty(navigator, ...). - False positives: extensions that override navigator values.
permissions_inconsistent
- Fires when:
Notification.permissionisdeniedwhilenavigator.permissions.query({ name: 'notifications' })saysprompt. - Meaning: a known headless Chrome trait; a normal browser keeps the two in step.
- False positives: rare; possible in browsers with unusual notification settings.
plugins_inconsistent
- Fires when:
navigator.pluginsornavigator.mimeTypesare not the nativePluginArrayandMimeTypeArraytypes, only one of them is empty, or a MIME type’senabledPluginis not one of the listed plugins. - Meaning: the plugin list was faked.
- False positives: none expected.
prototype_lie
- Fires when: the clean iframe sees through an override: its own
Function.prototype.toStringprints one of the page’s checked functions as non-native or with a different name (a Proxy keeps the name property but prints no name), or its native navigator and screen getters, applied to the page’s objects, return something other than what the page reads (or reject the page’s object, as they do for a Proxy). - Meaning: an override that the page’s own realm hides, for example by
also patching
toString. - False positives: as
native_tampering: extensions that change these functions.
software_renderer
- Fires when: the WebGL renderer or vendor names a software renderer (SwiftShader, llvmpipe, softpipe, “software rasterizer”, “basic render driver”, Mesa offscreen).
- Meaning: no GPU in use: common in servers, containers and virtual machines.
- False positives: real users with hardware acceleration turned off, a GPU the browser blocklists, remote desktops and some virtual desktop setups.
webdriver_writable
- Fires when:
navigator.webdriveris defined on the instance, or on the prototype as a data property, writable, or with a setter. Read from property descriptors only; it is never written. - Meaning: someone redefined
navigator.webdriver, usually to hide automation. - False positives: none expected in current browsers.
Using the flags
- Treat them as features for your own model or rules, not as a block list.
- Expect
native_tampering,prototype_lieandnavigator_own_propertyon real users with privacy extensions, andsoftware_rendereron real users without GPU acceleration. automation_webdriverwithautomation_globalstogether is a strong automation sign. Any single flag alone is weaker.- Flags come from the client: an attacker who controls the browser can remove them. Their presence is informative; their absence proves nothing. See Integration.
Left out on purpose
Two published checks are not used: Chrome DevTools Protocol detection needs
a console call (WhiskerJS writes nothing to the console), and a Web Worker
cross-check needs a blob: script that a page Content Security Policy may
block and report over the network. See the roadmap in the library source (docs/ROADMAP.md).