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.
