Use cases · WhiskerJS 0.1.0

Bot and headless detection

Fourteen read-only checks that hint at automation or tampering: how to read them, and why they are hints, not verdicts.

In plain words

Bots are programs that use websites like people do: signing up, logging in, buying stock. Many run in a “headless” browser with no screen. WhiskerJS runs fourteen quick checks for traces such tools leave and lists the ones that fired. A fired check is a hint worth weighing; no fired check does not mean a human.

The problem

Automated traffic ranges from your own monitoring and tests, through scrapers, to credential stuffing and scripted sign-ups. Simple tools leave obvious traces (navigator.webdriver set, a HeadlessChrome user agent). Better tools hide them and override browser values to look ordinary, which leaves other traces: functions that are no longer native, values that differ between the page and a fresh frame.

What WhiskerJS contributes

result.integrityFlags is a sorted list of the checks that fired. They run after the collectors, read only, and never write to the page; a check that throws simply does not fire. Two of them compare against a fresh, hidden about:blank iframe that is removed at once (see the clean iframe realm).

Signals and flags that matter

Flag Fires when (short) Known false positives
automation_webdriver navigator.webdriver is true your own end-to-end tests, accessibility scanners, monitoring robots
automation_globals a global or attribute left by a known automation tool a page script that happens to use one of the names (rare)
headless_user_agent HeadlessChrome, PhantomJS or SlimerJS in the user agent none expected; trivial to hide
headless_markers outer window size 0x0, or desktop Chrome UA without window.chrome some embedded Chromium views and webviews
permissions_inconsistent notifications denied while the permissions query says prompt rare; unusual notification settings
software_renderer WebGL names a software renderer such as SwiftShader or llvmpipe real users without GPU acceleration, remote and virtual desktops
webdriver_writable navigator.webdriver was redefined none expected
engine_mismatch engine traits do not match the browser class user-agent switcher extensions
iframe_mismatch page and fresh iframe disagree on navigator, screen or time zone values WebKit automation itself; top-frame-only extensions
native_tampering, prototype_lie, navigator_own_property built-in functions or navigator values were replaced privacy and anti-fingerprinting extensions, some polyfills
plugins_inconsistent, impossible_hardware faked plugin list, impossible CPU or memory values none expected in current browsers

A real example: Chromium run headless under Playwright in a container returned automation_webdriver, headless_user_agent, permissions_inconsistent and software_renderer. Its user agent said HeadlessChrome without window.chrome, so browser_class was other. The same setup with WebKit returned automation_webdriver and iframe_mismatch, the documented WebKit automation case.

Reading combinations

  • automation_webdriver together with automation_globals is a strong automation sign, per Using the flags.
  • native_tampering, prototype_lie and navigator_own_property together point at value overrides: a spoofing script or a privacy extension. Expect them on privacy-minded users.
  • software_renderer alone is common on real but unusual machines.
  • Any single flag alone is weaker than a combination.

Using structuredId similarity

Bot farms often run many sessions from the same image. Counting distinct accounts per visitorId, or per shared gpu and device parts of the structuredId, can show a fleet of identical headless machines. See the grouping example in Multi-accounting. Legitimate fleets (a call centre, a classroom) look similar, so combine this with rate and behaviour.

Anti-replay

A bot that cannot run the collector well may try to send a recorded result from a real browser. The binding makes that fail: the nonce is single-use and recent, and the digest covers the whole result. See Anti-replay binding. It does not stop a bot that runs a real browser and collects fresh.

What must happen on your server

  1. Treat flags as features, not a block list. Weigh them with request rate, IP and network reputation, header consistency and behaviour.
  2. Separate your own automation (tests, monitoring) by account, network or a header your server controls, not by trusting the flags.
  3. Choose graded responses: slow down, challenge, require login, or block, depending on the evidence and the cost of a mistake.
  4. Never treat integrityFlags: [] as proof of a human.

Limits

  • Flags come from code running on a machine you do not control. An attacker can strip them or send a hand-made result.
  • Two published techniques are left out on purpose: Chrome DevTools Protocol detection (it needs console output) and a Web Worker cross-check (it needs a blob: script that a page policy may block and report over the network). See Left out on purpose.
  • No detection or false-positive rates have been measured for WhiskerJS.

Questions and answers

Does WhiskerJS say whether a visitor is a bot?

No. It reports which checks fired. Your server weighs them with other evidence such as request rate, network reputation and behaviour.

Why do my own end-to-end tests show automation_webdriver?

Selenium, Playwright and Puppeteer set navigator.webdriver by default, so their sessions fire the flag. Exclude your test traffic by other means, such as a test account or network.

No flags fired. Is the visitor human?

Not necessarily. Flags come from the client, so an attacker can remove them, and careful tools leave none. Absence of flags proves nothing.

Is there a CDP or Web Worker check?

No. Chrome DevTools Protocol detection needs console output and the Worker check needs a blob: script that a page policy may block and report, so both are left out on purpose.