Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

👁 MickeyJS

Deep in-browser detector for Chrome DevTools Protocol traces and browser-automation artifacts.

31 probes across nine families — CDP protocol side-channels, input-event forensics, behavioural biometrics, framework globals, stack signatures, cross-realm consistency, anti-detect tamper forensics, headless environment tells and cross-signal consistency.

▶ Live demo · Signal catalogue · MIT


What it is

A zero-dependency library that answers two questions about the browser it is running in, and keeps them separate because they need different evidence:

  1. Is this browser being driven by something? → a 0–100 score and a verdict.
  2. By what? → a ranked attribution across Puppeteer, Playwright, Selenium/ChromeDriver, Pyppeteer, Cypress, Electron, an open DevTools window, a generic CDP client, an anti-detect patch layer, or a synthetic-input agent.

Every finding comes back as a signal with its raw evidence attached, so a report is auditable rather than a number you have to trust.

score        99.5 / 100
verdict      automated
attribution  Puppeteer 100%  ·  CDP client 93%  ·  Synthetic input 79%
signals      6 (2 definitive)
probes       31/31 ran in 415 ms

Why it exists

Most published automation-detection code is a copy of a 2024 blog post, and a surprising amount of it silently stopped working. Two examples that this library handles explicitly rather than pretending otherwise:

The classic CDP oracle is dead — and something else still works. The famous Runtime.enable check planted a getter on error.stack and logged the error. Two V8 commits in May 2025 stopped the inspector invoking user getters during error preview, and the signal went quiet around Chrome 138 without much notice. MickeyJS probes both accessors and reports which fired. Verified against Chrome 151: stack reads 0, Error.prototype.name reads 14. The name channel is not covered by the guard and still detects Runtime.enable today.

The CDP input coordinate leak was fixed in Chrome 142. pageX === screenX (crbug#1477537) was the single best injected-input tell for years. chromium-review 6917162 fixed it, and started emitting coalesced events for injected input at the same time. MickeyJS version-gates both affected probes and says so in the evidence, instead of reporting a clean result as if it meant something.

The general principle: a probe that cannot run says so. It is never silently counted as a pass.

Install

npm install mickeyjs

or drop in the bundle — no build step, no dependencies:

<script src="https://cdn.jsdelivr.net/npm/mickeyjs/dist/mickeyjs.min.js"></script>

Usage

One-shot

import { detect } from 'mickeyjs';

const report = await detect();
console.log(report.verdict, report.score, report.attribution);

Recommended — start early, scan later

Passive collectors want to be live before the visitor interacts, and CDP clients frequently attach after page load:

import { Detector } from 'mickeyjs';

const detector = new Detector();
detector.start();                 // install passive collectors + hooks (synchronous, cheap)

// …later, e.g. at form submit or checkout
const report = await detector.scan();
if (report.score > 70) flagSession(report);

Live monitoring

detector.watch((report) => {
  console.log(report.verdict, report.score);
}, 2000);

Script tag

The IIFE bundle exposes the whole namespace as window.MickeyJS:

<script src="./dist/mickeyjs.min.js"></script>
<script>
  MickeyJS.detect().then((r) => console.log(r.verdict, r.score));
</script>

Options

new Detector({
  hooks: true,      // patch DOM entry points for the stack-signature probes (default true)
  behavior: true,   // record input events for the input/behaviour families (default true)
  exclude: [],      // probe ids to skip
  only: null,       // restrict the run to these probe ids
});

detector.stop() reverts every hook and removes every listener.

The report

{
  "version": "1.0.0",
  "score": 99.5,
  "verdict": "automated",          // automated | likely-automated | suspicious | inconclusive | clean
  "summary": "Browser is under automation control.",
  "definitive": ["framework.playwright.globals"],
  "attribution": [
    { "actor": "playwright", "label": "Playwright", "confidence": 100, "signals": [...] }
  ],
  "signals": [
    {
      "id": "cdp.errorPreview",
      "family": "cdp",
      "label": "Error preview read Error.prototype.name (post-guard CDP channel)",
      "confidence": "strong",
      "weight": 0.85,
      "actors": ["cdp", "devtools"],
      "evidence": { "stackGetterReads": 0, "nameGetterReads": 14, "chromeMajor": 151, "channel": "name" },
      "note": "An inspector serialised a logged Error and read its `name` accessor…",
      "ref": "V8 commits 2025-05-07 / 2025-05-09",
      "at": 128.4,
      "observations": 3
    }
  ],
  "families": { "cdp": {...}, "input": {...}, ... },
  "probes":  [ { "id": "realm.worker", "status": "ok", "ms": 42, "fired": 0 } ],
  "environment": { "userAgent": "...", "chromeMajor": 151, ... },
  "interaction": { "moveEvents": 240, "clicks": 3, "rawUpdates": 180 }
}

Probe families

Family Probes What it looks at
cdp 4 Runtime.enable console-serialisation side effects and cost; debugger pause timing in a Worker
input 1 Provenance artifacts of injected pointer/touch events
behavior 1 Mouse-event rate, click dwell variance, teleport clicks, path linearity, typing rhythm
framework 3 Driver globals, $cdc_ patterns, <html> attributes, exposeFunction bridges, Node runtime
stack 3 pptr: / UtilityScript. / Proxy-trap frames; ChromeDriver's injected bundle; main-world canary
realm 2 Does navigator agree across window, Worker and a fresh iframe
tamper 3 Native-function integrity, cross-realm toString, Proxy failure modes, accessor timing
env 12 Automation flag forensics, headless build identity, surface geometry, GPU, codecs, CSP enforcement
consistency 2 OS / timezone / locale / touch agreement across independent sources

Full detail — every signal, its tier, its actors and its references — is in SIGNALS.md.

How scoring works

The score is a noisy-OR combination of independent evidence:

P(automated) = 1 − Π(1 − wᵢ)

This is deliberate. Additive scoring lets a pile of weak environment tells — small screen, few fonts, no GPU, unusual core count — manufacture a confident verdict out of nothing, which is how real anti-bot systems end up blocking people on old laptops in VMs. Under noisy-OR, four weak signals reach 76, not 120.

Three further rules keep the model honest:

  • A definitive signal pins the score to 100. __pwInitScripts does not appear in a browser that Playwright is not driving. No amount of contrary evidence dilutes that.
  • Repeat observations count once. Seeing the same artifact on five scans is one piece of evidence, not five.
  • Family diversity corroborates but cannot convict. Signals spread across three or more families get a small bump, capped at 95, and zero signals stay at zero.

Attribution is scored separately, and a signal naming several possible actors splits its weight between them — so "could be Puppeteer or Playwright" never reads as strong evidence for both.

Honest limitations

Please read this section before deploying anything that blocks people.

  • A clean result is weak evidence. Patched drivers (rebrowser-patches, Patchright, nodriver, Camoufox) exist specifically to defeat these checks, and they largely succeed against the cheap families. A silent framework family plus a live cdp family is itself the fingerprint of a patched setup.
  • Isolated worlds hide from hooks. Every stack probe depends on driver code running in the main world. Patched drivers evaluate in an isolated world and are invisible to them by construction.
  • DevTools and CDP drivers share most signals. The serialisation channels cannot tell a developer with DevTools open from Puppeteer. cdp.debuggerPause is the disambiguator; the attribution list shows both with their separate confidences.
  • Behavioural probes need interaction. They stay silent otherwise, by design.
  • env signals are corroboration, not evidence. Software rendering, sparse fonts and empty device lists are all genuine on real machines — VMs, remote desktops, hardened Linux desktops, GPU blocklists. That is why they are weighted the way they are.
  • This is an arms race with a maintenance bill. Two of the strongest signals in the field changed status between 2025 and 2026. Anything here can be patched out by the next Chrome release or the next stealth build.
  • It cannot see the network layer. TLS/JA4 fingerprints, HTTP/2 SETTINGS ordering, header ordering and sec-ch-ua consistency are strong, complementary signals that JavaScript simply cannot reach. Serious deployments should combine both.

What it deliberately does not do

  • No browser-crashing payloads. Known crash-the-driver techniques exist; crashing a visitor's browser is not a detector's job.
  • No network requests. Nothing is uploaded, no beacon, no third-party call. The entire scan is local.
  • No device fingerprinting for identity. Canvas/audio/WebGL hashing to identify which device this is, rather than whether it is automated, is a different product with different consent implications.

Known console noise

The CSP-bypass probe intentionally attempts one policy violation. On a page with a strict CSP you will see one blocked-resource error in the console — that error is the probe reporting that enforcement is intact. Disable it with new Detector({ exclude: ['env.cspBypass'] }).

Development

npm install
npm run build        # dist/mickeyjs.js, .min.js, .esm.js
npm test             # engine, scoring, registry and catalogue-completeness tests
npm run serve        # build + serve the demo on http://localhost:8099

The test suite verifies structural correctness rather than browser behaviour: every probe is well-formed, the scoring model behaves under adversarial inputs, a throwing probe cannot break a scan, a hanging probe is timed out, and SIGNALS.md documents exactly the set of signals the probes emit — in both directions, so the catalogue cannot silently drift.

Verified end-to-end against a real Chrome 151 driven over CDP, where it correctly returns automated, attributes Puppeteer from a pptr: stack frame, and runs all 31 probes in ~415 ms.

Credits

This builds directly on published research, and each probe carries a ref to its source:

License

MIT © MickeyAlton33

About

Deep in-browser detector for Chrome DevTools Protocol traces and browser-automation artifacts. 31 probes: CDP side-channels, input-event forensics, behavioural biometrics, framework artifacts, cross-realm consistency, anti-detect tamper forensics.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages