# Integrity checks

> 14 read-only checks for automation and tampering, and when they can be wrong.

Source: https://whiskerjs.com/docs/integrity/ (last updated 2026-10-26)

> **In plain words:**
>
> 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, hidden `about:blank` iframe that is removed straight away (see [How it works](https://whiskerjs.com/docs/how-it-works/#the-clean-iframe-realm)); 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 `document` property starts with `$cdc_`, `$wdc_` or `cdc_` (ChromeDriver), or `<html>` has a `webdriver`, `selenium` or `driver` attribute.
- **False positives**: a page script or extension that happens to define a global with one of these names (for example `geb` or `awesomium`). Rare.

### `automation_webdriver`

- **Fires when**: `navigator.webdriver` is `true`.
- **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 `eval` is 33 characters in V8 and 37 in Gecko and WebKit, and `navigator.productSub` is `20100101` in Gecko and `20030107` elsewhere. Not checked for class `other`.
- **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**: `outerWidth` and `outerHeight` are both 0, or the user agent is desktop Chrome but `window.chrome` is 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`, `PhantomJS` or `SlimerJS`.
- **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**: `hardwareConcurrency` is a number but not a whole number from 1 to 256, or `deviceMemory` is 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 its `name` property, or it has extra own properties. Checked: `Function.prototype.toString` first (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 getters `userAgent`, `platform`, `languages`, `hardwareConcurrency`, `deviceMemory`, `webdriver`, `plugins`, the screen getters `width`, `height`, `colorDepth`, and `HTMLElement.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`, `vendor` or `webdriver` is defined on the `navigator` object itself instead of its prototype.
- **Meaning**: overridden with `Object.defineProperty(navigator, ...)`.
- **False positives**: extensions that override navigator values.

### `permissions_inconsistent`

- **Fires when**: `Notification.permission` is `denied` while `navigator.permissions.query({ name: 'notifications' })` says `prompt`.
- **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.plugins` or `navigator.mimeTypes` are not the native `PluginArray` and `MimeTypeArray` types, only one of them is empty, or a MIME type’s `enabledPlugin` is 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.toString` prints 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.webdriver` is 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_lie` and `navigator_own_property` on real users with privacy extensions, and `software_renderer` on real users without GPU acceleration.
- `automation_webdriver` with `automation_globals` together 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](https://whiskerjs.com/docs/integration/#what-never-to-trust-from-the-client).

## 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`).
