All articles Bot detection

puppeteer-extra-stealth: Which Evasions Still Leak

puppeteer-extra-stealth is the reference implementation of browser evasion, and most other stealth kits are downstream of its module catalog. It works by loading a chain of small patches, each hiding one automation tell, which means detecting it is largely about finding the modules that patch imperfectly or the tells no module covers.

The module model

Stealth is not one monolithic disguise. It is a plugin that registers evasions like navigator.webdriver, chrome.runtime, navigator.plugins, webgl.vendor, media.codecs, and iframe.contentWindow. Each runs an override at document start. Because they are independent, they can disagree with each other, and because they patch known tells, they tell a detector exactly where the author expected scrutiny.

Evasions that hold up well

Credit where due, several modules are solid:

  • navigator.webdriver removal is clean when done via prototype deletion rather than a getter.
  • navigator.languages and navigator.plugins produce plausible arrays.
  • window.chrome injection creates a believable-enough runtime shell for shallow checks.

If your detection stops at “is webdriver true,” stealth wins outright. That is why presence checks alone are obsolete.

Evasions that still leak

The residue clusters in a few places:

  1. Function toString integrity. Every wrapped native needs Function.prototype.toString spoofed too. Miss one wrapper and the patched method stringifies to injected source instead of [native code]. The iframe.contentWindow and webgl evasions historically leaked here.
  2. WebGL self-contradiction. Spoofing UNMASKED_RENDERER to a common GPU does not update the extension list, max render buffer size, or shader precision, so the claimed and measured hardware disagree.
  3. Codec and media inconsistencies. The media evasion can report support for codecs the underlying Chromium build does not actually decode, a mismatch detectable by probing canPlayType against real decode behavior.
  4. New-property lag. Chrome ships new navigator and window surfaces every few releases. Stealth patches the old catalog; a fresh Chrome property left in its automation-default state is a clean tell until the module list catches up.

The signals no module touches

The most durable detection lives where stealth has no module at all:

  • CDP artifacts. puppeteer drives over the DevTools Protocol. Execution-context timing, target creation, and exception-handling quirks persist regardless of page-level patching.
  • Behavioral entropy. Cursor paths, scroll physics, and keystroke timing stay machine-regular no matter how clean the fingerprint.
  • Environment provenance. A value a script controls can be spoofed. How the browser was launched and hosted cannot be spoofed from inside the page.

This is the argument for server-side verification over client trust: the browser reports what it is told to report, so decisions belong on infrastructure the bot does not control.

Correlation beats per-property patching

Here is the structural problem for any stealth operator. Even a flawless per-property disguise does not change that the same puppeteer image, launch flags, and small pool of hardware profiles power the whole farm. Prynt’s stable visitorId ties sessions together across proxy rotation, incognito, and storage clears, so a scraper presenting a thousand pristine fingerprints that collapse to a dozen real identities is caught by the collapse, not by any one property. The cross-site reputation network then means a stack burned elsewhere shows up already suspect.

The maintenance treadmill for operators

There is an economic point buried here. Every Chrome release potentially adds a new surface stealth must patch and shifts the behavior of one it already patches. The stealth maintainers chase those changes, and the operators running the plugin have to keep upgrading to stay current. Miss a release cycle and a freshly shipped navigator property sits in its automation-default state, a clean tell. Detection, by contrast, does not have to enumerate every leak. It scores categories, integrity, contradiction, provenance, behavior, identity, that stay stable across releases. That asymmetry favors the defender: the bot operator must patch every tell perfectly and forever, while the detector only needs enough correlated evidence to cross a confidence threshold. It is far cheaper to score five weak signals than to forge all five, which is why correlated server-side scoring has quietly won the arms race that per-property patching started.

A detection recipe

To catch stealth-patched puppeteer reliably:

  1. Replace presence checks with consistency checks across correlated properties.
  2. Verify native-function integrity, including toString on every candidate wrapper.
  3. Cross-check WebGL claims against measurable capabilities.
  4. Score CDP and behavioral signals server-side.
  5. Correlate the visitorId across sessions and against reputation data.
  6. Emit reason codes so analysts can see why a session scored high.

The bottom line

puppeteer-extra-stealth is excellent at defeating naive, single-property detection and nearly useless against correlated, server-side scoring. The modules that leak change release to release, but the categories that leak, integrity, contradiction, and provenance, do not.

Curious how a stealth-patched puppeteer session scores against correlated signals? Try it in the playground, or see plans on our pricing page. Free to start.

Try it free

Prynt is device intelligence with a free tier — visitor IDs, bot & fraud Smart Signals, and behavioral biometrics, powered by a cross-site network. Start free.

Keep reading