Partial fingerprint spoofing detection works by asking one question two ways. If a browser changes the answer on one route and leaves the other alone, a page that asks both gets two answers, and no real browser disagrees with itself. We removed or cut back five spoofs in our engine for exactly this reason. Each one could cover only one of the routes a page uses to read the same fact.

Fonts: listed one way, measured another

A page can ask whether a font is installed with document.fonts.check. It can also draw a string in that font and compare its width with the same string in a fallback font. If the widths differ, the font is there. That second way is how fingerprinting scripts actually enumerate fonts.

Our old patch answered document.fonts.check from the profile's generated font list and did nothing about measurement. A page asking both ways could find a font that reports absent and renders, or reports present and renders as the fallback. Either pairing is impossible on a real machine.

Keyboard: the size and the type gave it away

navigator.keyboard.getLayoutMap() returns a map of keys to the characters they produce. Our patch returned six entries, the keys that differ between common layouts. A real one returns about fifty, so the size alone identified the browser. It also returned a plain Map, where the real API returns a KeyboardLayoutMap. The prototype said it a second time.

Media codecs: a downgrade the hardware contradicts

Our codec table was consulted in both directions. So a Mac that decodes HEVC in hardware answered "no" when a page probed for H.265, while the hardware itself could play the file. The table now only ever upgrades: it can add a proprietary codec a Chromium build is missing, and never removes one the engine really supports.

The fingerprint is changed inside the engine, not painted on topIn Mosaic: Its own Chromium. Tesseract Chrome: values changed in C++, not JavaScript. Free holds 3 profiles. Pro is $49 a month for any number.

How it works: Its own Chromium

Speech voices: right on desktop, wrong on Android

The voice spoof was a fixed table of 45 macOS voice names. Thirteen were not installed on our build Mac, and a few, Fiona and Victoria among them, Apple had dropped years earlier. A voice set that matches no real install of the claimed system is the same mistake as claiming a browser version whose features are missing. We removed the table, and desktop profiles report the computer's own voices, which is the right answer when the profile claims the system the computer runs.

That reasoning missed one case. An Android profile and a macOS profile drawn on the same Mac both reported 180 voices, byte for byte the same, and the voices were Apple's: Albert, Alice, Alva, Amélie. No Android phone has reported Amélie, and getVoices() needs no permission. The engine already had a switch for voices. What it lacked was data about real phones, and one honest answer needs none: a device with no speech engine. Android profiles now send --fingerprint-voices=none and report zero voices. That is less common than a phone with voices, and it is possible, which the Mac's list on a phone was not.

The camera: a filter can only remove

The camera spoof filtered the engine's device list by counts the generator drew. A filter can only take things out, so its one possible effect was to say "this machine has no camera", on a machine with one, while getUserMedia went on opening it. It also leaned the implausible way: a desktop with a webcam plugged in is ordinary, and a MacBook Pro without a camera does not exist. Our comparison against stock Chrome caught it intermittently, only when a draw landed on a cameraless desktop:

stock  audioinput,videoinput,audiooutput
ours   audioinput,audiooutput
Settings showing the installed Tesseract Chrome engine
The values that stay are changed in the engine, in C++

What partial spoofing detection teaches

Only spoof a surface if every route to it can be made to agree. Where that is not possible, report the machine's truth. Two profiles on one computer then share their fonts, keyboard and codecs, as they already share the TLS handshake and other values that come from the real machine. A shared truth gives a site less to object to than a private contradiction.

This is also why Tesseract Chrome changes values in C++ rather than in page JavaScript: a value changed inside the engine reaches every frame and worker, so the routes agree by construction. C++, not JavaScript has the case where a JavaScript spoof gave its own source away to an iframe, and the engine page lists what the patches cover. The same thinking retired three more invented values (the storage quota, performance.memory and navigator.connection), each of which a page could check by measuring the thing it described.

How detectors look for these contradictions in general is in how sites detect anti-detect browsers. The values we found coming from the computer rather than the profile are in the values every profile inherited from this Mac.