Skip to content
Benchmarks

Your noise floor is too small: a fingerprint field that was random all along

Two identical runs are enough to spot most session values. They are not enough for a field that samples randomness, and one of those looked exactly like a tell.

Pim
Pim· Clearcote Research
6 min read

Key takeaways

  • Math.random().toString(36).length is 13 about 70.5 % of the time, 12 about 26.3 %, 14 about 2.4 % and 11 about 0.7 % (100,000 draws), in stock Chrome 156 and a patched Chromium alike.
  • Seeing 12 twice in a row has a probability of about 7 %; seeing 13 five times in a row about 17 %. Neither says anything about the browser.
  • A noise floor needs to classify every field as constant, per-session or random, and random fields need a distribution, not a pair of runs.

A field that looked like a tell

We compared what a deployed challenge widget collects from stock Chrome 156 and from a patched Chromium build, probe by probe, after decoding both payloads. To separate real differences from noise we first diffed two stock Chrome runs against each other: whatever changed between them, such as URLs, timestamps and timings, was session noise and could be ignored.

One field survived that filter. In a probe full of JavaScript and DOM quirk checks, index 75 was 13 in five stock Chrome runs, 13 in a recorded iPhone, and 12 in both captures of the patched build, on two different personas. Constant across stock runs, different in the patched build: by the method above, a tell.

We tested every plausible explanation we could think of, all the plugin and mime-type counts and prototype key lists around that position in the probe, and all of them were identical in both browsers. The field did not fit any of them.

What it actually was

Disassembling the probe's bytecode answered it. Right after a check of sloppy-mode this boxing, the probe pushes Math.random().toString(36).length: the length of a random number written in base 36.

That length varies because the number of digits needed to print a double exactly depends on its value. Over 100,000 draws the distribution was the same in both browsers: 13 about 70.5 % of the time, 12 about 26.3 %, 14 about 2.4 % and 11 about 0.7 %. The patched build drawing 12 twice had a chance of about 0.263² ≈ 7 %. Stock Chrome drawing 13 five times in a row had about 0.705⁵ ≈ 17 %. Nothing unusual happened, and the field cannot tell any two browsers apart that share V8's random number generator.

Why it is in the probe at all is a separate question. It may serve as a cheap check on the generator, or as an entropy source for something else in the payload. For comparing browsers it is noise.

Three kinds of field

The mistake was treating every field as one of two kinds. There are three:

  • Constant for a given browser on a given machine: Math output, fonts, WebGL parameters, Intl strings. Two runs are enough; any difference is real.
  • Per-session: URLs, timestamps, timings, nonces. They change every run, so two runs show them changing and a diff ignores them.
  • Random draws from a fixed distribution: values that change only sometimes. With a common value, two runs often agree by chance and the field passes as constant; a third run in another browser then disagrees, also by chance.

The third kind is the dangerous one, because it looks like the first until the sample is large enough.

A better noise floor

  • Run the stock browser many times, not twice: ten captures show a 70 % value differing at least once with high probability.
  • Identify the expression behind any field that differs before calling it a tell. A field whose meaning is unknown is a hypothesis, not a finding.
  • Sample suspected random fields directly. Running the expression 100,000 times in both browsers took seconds and settled the question; comparing two distributions is the only valid test for a random field.
  • Record the null result. A field shown to be random should go on a list so the next comparison does not rediscover it.

The same applies in the other direction: a real tell can hide inside a random-looking field when the distributions differ, for example a randomness source that is seeded identically in every session. Only the distribution shows that.

The through-line

Measurement claims need the same rigour as the systems they measure. How browser stealth is measured covers repeatable setups and pinned inputs; this is the statistical half of the same discipline. The full probe comparison is summarised in inside a challenge widget, and the fingerprint test marks its own random and environment-dependent rows as informational for the same reason.

#benchmarks#fingerprinting#reference

Clearcote puts this into practice

An open-source Chromium with fingerprint control compiled into the engine. A drop-in for Playwright & Puppeteer.

  • 222engine patches
  • 34,478lines added
  • 330Chromium files changed
  • 354checks per build

Engine numbers are for the Pro build; the free open build is a smaller, public patch set.

Free for one browser with GitHub. No card.