The three classes of lie
1. Tampering lies. When a script replaces a built-in function - say navigator.webdriver or HTMLCanvasElement.prototype.toDataURL - the replacement no longer prints as [native code] the way a genuine browser function does. Asking Function.prototype.toString.call(fn) what the function looks like - and checking that toString itself has not been tampered with - exposes the patch. See function toString inspection.
2. Contradiction lies. Two reported values cannot both be true: a Windows User-Agent paired with a Linux font set, a navigator.platform of Win32 but a math signature (tiny rounding differences unique to each JS engine) from a different engine, userAgentData.mobile = true alongside maxTouchPoints = 0 (a touchscreen with zero touch points), or a screen availWidth larger than its width.
3. Scope lies. The most elegant: spawn a Web Worker (a background JavaScript thread) and read navigator from inside it. Many spoofing tools only patch the main-thread navigator and forget the worker scope, so the two disagree. CreepJS leans heavily on this.
Why lie detection beats spoofing
Single-value spoofing assumes the vendor reads each signal on its own. Lie detection assumes nothing and instead measures coherence - whether everything fits together. To pass, a scraper must present a fingerprint where every signal - UA, platform, fonts, canvas, WebGL renderer, math, timezone, languages, worker scope - matches one real, existing device. That is why the durable approach is to run a genuine browser on genuine hardware (or a deeply patched build like Camoufox / CloakBrowser) rather than overriding properties at runtime.
You can see exactly which lies your own browser exposes - and the trust score they add up to - in the Browser Fingerprint Checker.
Why coherence is the unit of measurement
The lesson from lie detection is that detectors measure the whole identity, not any single field. Once a detector cross-checks the User-Agent against the JS engine math, the font set against the OS, the GPU string against the renderer, and the timezone against the IP geolocation, a value changed in isolation only creates a new contradiction. Any field that differs has to be consistent with every field that did not.
This is why tools built around a real, internally consistent device profile — the approach managed scraping APIs and patched browsers such as Camoufox take — behave differently from runtime property overrides. A coherent stack (engine, fonts, canvas, WebGL, headers, network) has no seam for cross-checks to catch, whereas JavaScript overrides layered on top of a headless Chrome still surface contradictions the detector can read.
The four questions a lie detector asks
Lie detection is not one test but a small family of them, each attacking the claim from a different direction. Taken together they cover the ways a stated identity can fail to match the machine underneath.
| Question | Method | What a failure proves |
|---|---|---|
| Do two APIs agree? | Ask the same question through unrelated code paths and compare - see cross-API coherence checking | One surface was changed and another was not |
| Do all realms agree? | Re-read the same fields inside a Worker and a fresh iframe | A change was applied to the page global only |
| Are the built-ins genuine? | Serialisation, descriptor shape, receiver brand checks, honeypot names | Engine functions were replaced by JavaScript |
| Does the claim match the derived values? | Compare a stated browser/OS against constants no header can move | The declared identity is not the binary that is running |
The fourth is the hardest to satisfy and the least discussed. Values like eval.toString().length, navigator.productSub, and the rounding of transcendental math are derived from how the engine was compiled and which system libraries it links against, so they cannot be set per session. A stated identity has to describe the binary that is genuinely executing, or these engine and OS oracles contradict it before any fingerprint hash is even computed.
Why a lie costs more than the truth it replaces
The asymmetry at the centre of lie detection is that a declared value is one number while a coherent identity is a web of constraints. Changing the declared value is a single edit; keeping the web consistent means also moving everything that is computed from the same underlying fact, in every realm, through every code path, including paths that were never designed as identity and that nobody thinks of as part of a fingerprint.
Consider a change to the reported platform. It has to hold for navigator.platform, the Sec-CH-UA-Platform request header, the high-entropy client hint, the CSS system colors, the default font stack and its glyph advance widths, the hyphenation break points, the math library rounding, the shape of runtime error messages, and the same set of values again inside every Worker and iframe. Miss one and the identity is not merely rare - it is impossible, which is a far stronger signal.
This is why detection has shifted away from rarity scoring. A rare fingerprint has an innocent explanation: unusual hardware, an old browser, an accessibility configuration. A contradiction has none, so a site can act on it with much more confidence and far fewer false positives. The measurable consequence is that a partially modified environment often scores worse than an unmodified one, because the unmodified one is merely identifiable while the modified one is provably inconsistent.
The durable answer is to make the claim true rather than to defend it: run a real engine on a real operating system with genuine locale and network settings, so the derived values agree because they were never in conflict. That is the design principle behind engine-level browser builds, and it is what a managed web data API provides without the fleet becoming your problem.
