StableBrowse Engineering

The browser fingerprinting toolkit

TLS impersonation, binary-patched browsers, machine native browsers, and the coherence layer that ties them together.

← Engineering index

05 / 05

The browser fingerprinting toolkit

Browser fingerprinting is no longer one problem. It is a stack.

There is the network fingerprint: TLS, HTTP/2 settings, header order, proxy geography. There is the browser fingerprint: canvas, WebGL, AudioContext, plugins, fonts, screen state, timing APIs. There is the automation fingerprint: CDP artifacts, Playwright globals, navigator.webdriver, command-line APIs. And then there is the session fingerprint: behavior, history, IP reputation, and whether the whole thing looks coherent over time.

That is why the tooling landscape looks fragmented. TLS impersonation libraries, patched browsers, profile systems, and machine native browser layers are not really competing answers to one question. They each handle a different part of the stack.

The useful way to think about them is not “which one wins?” It is “which layer does this make more coherent?”

Fingerprinting problem space → Tooling map:

  ┌────────────────────────┐
  │   Machine Native       │  ← Coherence layer (our bet)
  │   Browser              │    Session protocol · Identity graph
  │                        │    Routing logic · Verification harness
  ├────────────────────────┤
  │   Browser APIs         │  ← Binary-patched browsers
  │   Canvas · WebGL       │    Native code paths, not JS shims
  │   Audio · Plugins      │    Profile systems for consistency
  ├────────────────────────┤
  │   Network Identity     │  ← wreq (Rust + BoringSSL)
  │   TLS · HTTP/2         │    100+ browser-emulation profiles
  │   JA3/JA4              │    Chrome-shaped handshakes
  ├────────────────────────┤
  │   Session & Behavior   │  ← Action models · IP reputation
  │   Mouse · Typing       │    Session history · Return patterns
  │   Scroll · History     │    Proxy geography alignment
  └────────────────────────┘
Fig 1. Each layer of the fingerprinting stack maps to a different class of tooling. No single tool covers the full stack.

The pieces of the stack

Network identity: wreq

For headless HTTP clients — scrapers, agents, test runners, or other systems that do not run a full browser — wreq addresses the TLS fingerprinting problem. It is a Rust HTTP library that replaces rustls with BoringSSL and ships 100+ browser-emulation profiles.

The point is not just “make a request.” The point is to make the request look like it came from the browser identity it claims to be. With the right profile, wreq can produce a Chrome 137-shaped handshake: correct cipher suite ordering, GREASE values, Chrome-specific extensions such as ALPS and compressed certificates, and matching HTTP/2 SETTINGS.

!

rustls won’t help here. The maintainers of rustls explicitly closed the issue requesting browser-fingerprint support in January 2026, saying they do not intend to support that use case. For Rust HTTP clients that need browser-shaped TLS, wreq is currently the practical path.

It solves one layer. It does not solve JavaScript challenges, browser API consistency, or behavior. If a site serves a challenge script, the session still needs a real JavaScript environment with real browser APIs.

Browser API consistency

The browser-level problem is making probed APIs return values through normal browser code paths instead of page-level JavaScript shims.

JavaScript overrides are fragile. If navigator.webdriver or WebGLRenderingContext.getParameter is changed by injected code, that injection can itself be detected. CreepJS has a whole “lies detector” module for this class of problem. It checks function .toString() output, descriptor configurability, prototype chain mutations, and other signs that a value was patched from the page side.

// What CreepJS checks for patched values:

Function.prototype.toString.call(navigator.__lookupGetter__('webdriver'))
// Expected: "function get webdriver() { [native code] }"
// Patched:  "function () { return false }" ← detected

Object.getOwnPropertyDescriptor(navigator, 'webdriver')
// Expected: undefined (inherited from prototype)
// Patched:  { value: false, configurable: true } ← detected

navigator.__proto__.hasOwnProperty('webdriver')
// Expected: true (on prototype)
// Patched:  depends on where the override was placed
Fig 2. CreepJS “lies detector” checks. Page-level JavaScript patches are detectable through multiple reflection techniques.

This is what binary-level browser work solves. It moves the answer from the page layer into the engine layer. Canvas, WebGL, AudioContext, fonts, GPU strings, screen properties, WebRTC, and network timing can behave like browser internals instead of values painted over after page load.

Profile systems solve a related problem: consistency across the browser identity. A browser identity is not just a User-Agent string. It is GPU, platform, fonts, plugin list, screen dimensions, time zone, locale, storage, WebRTC behavior, and proxy geography all moving together. If a Windows profile runs on a Linux server but the time zone, WebGL renderer, and proxy location disagree, the identity is incoherent.

The machine native browser

The machine native browser changes the framing again. The hard part is not just picking the right browser engine. It is making the network identity, browser profile, session state, actions, and observations agree across the whole workflow.

Machine Native Browser — Control Plane Architecture:

  ┌─────────────────────────────────────────────────┐
  │              Session Protocol                    │
  │  Identity graph · Routing logic · Verification  │
  └──────────┬──────────────┬───────────────┬───────┘
             │              │               │
     ┌───────▼──────┐ ┌────▼────────┐ ┌────▼────────┐
     │  wreq        │ │  Patched    │ │  Profile    │
     │  (HTTP)      │ │  Browser    │ │  Context    │
     │              │ │             │ │             │
     │  When full   │ │  When JS    │ │  When       │
     │  browser     │ │  challenges │ │  identity   │
     │  not needed  │ │  require    │ │  coherence  │
     │              │ │  real engine│ │  is critical│
     └──────────────┘ └─────────────┘ └─────────────┘

  The control plane decides which backend to use per request.
  All backends share one identity and one session model.
Fig 3. The machine native browser selects execution backends based on what the session requires, while maintaining a single coherent identity.

This is where the machine native browser becomes its own product surface. It decides when a plain fetch is enough, when a full browser session is required, which identity profile belongs to the session, how proxy geography maps to time zone and locale, how actions should be emitted, and how failures get diagnosed instead of retried blindly.

What still doesn’t work

There are still hard gaps.

Gap Why it’s hard Current state
Canvas fidelity Many implementations return a hard-coded 1×1 PNG for canvas.toDataURL() Requires tracking draw-call state and producing output that reflects the drawing operations
Long sessions WebGL fingerprint spoofing degrades over time Detection systems maintain expected output hashes per GPU model; small differences become statistically meaningful
IP reputation Perfect browser surface on a high-risk DC origin is still high-risk Network location, timezone, locale, WebRTC candidates, and history need to agree
Behavioral modeling One plausible session is achievable; a population of realistic sessions is much harder Return patterns, browsing history, session lengths, and aggregate behavior are hard to model at scale
Engine scoring Some scoring systems treat Chromium-shaped traffic differently from Firefox-shaped Not purely a fingerprinting problem; part of it is policy

How we handle the problem

The practical answer is not to wrap all of these tools and call it a product. That would be fragile, and it would not solve the core problem.

Our bet is the machine native browser needs a coherence layer.

The underlying engines matter, but the product is the system around them: identity management, session continuity, backend selection, action semantics, failure diagnosis, and verification. That is the part that turns a pile of powerful browser tools into a machine native browser agents can use reliably.

The core is ours: the session protocol, observation model, action model, identity graph, routing logic, verification harness, and recovery loop. We decide whether a request should go through a browser-shaped HTTP client, a patched browser, a profile-based browser context, or a lighter extraction path. We track what identity the session is supposed to be presenting, then make the network, browser profile, storage, locale, time zone, and behavior line up with that identity.

Our approach is to make coherence a first-class system property:

  • Network requests should match the browser identity before HTML is served.
  • Browser identity should be consistent across User-Agent, platform, GPU, fonts, timezone, locale, screen, WebRTC, and storage.
  • JavaScript-visible APIs should come from the right backend layer, not brittle page-level patches.
  • Agent actions should be emitted with realistic timing and recoverable state transitions.
  • Observations should be structured enough for agents to reason over, not just pixels and screenshots.
  • Failures should be diagnosable by layer: network, challenge, browser API, behavior, or site logic.
  • Session behavior should be modeled at the workflow layer instead of treated as an afterthought.
i

One control plane, multiple backends. Existing tools prove the market has pain at every layer. Our work is turning those scattered lessons into a machine native browser: one control plane, multiple execution backends, and a single coherent session model. That is the credibility gap we are trying to close.

Want to go deeper?

We'll walk you through the architecture behind your workflow.