CreepJS's prototype page walks the browser's built-in objects, Navigator, Screen, canvas, WebGL, Date, Intl and dozens more, and asks each property whether it still looks native. A "lie" is any getter or method whose own keys, source text or error behaviour differ from what the real browser ships. It names the prototype and property, so a spoof written in JavaScript gets caught by name, while a value changed in the engine's C++ leaves it nothing to find.
What the prototype page actually tests
CreepJS's lie detector lives in its source as one list of tests applied to every target it names. For Navigator alone the list includes userAgent, platform, hardwareConcurrency, deviceMemory, languages, maxTouchPoints, plugins and webdriver, which are exactly the properties a fingerprint spoof wants to change.
For each one it reads the getter or method and asks questions a real native function answers the same way every time:
- Does
Function.prototype.toStringprint it as[native code], and doestoStringitself print as native? - Are its own property names exactly
lengthandname? A function written in JavaScript usually hasprototypetoo, and sometimesargumentsorcaller. - Does
'prototype' in fncome back false, as it does for a native getter? - Does calling it the wrong way throw the right
TypeError:new fn(),fn.call()on the prototype,class extends fn, reading the getter straight off the prototype? - Does it behave like a Proxy? Setting its prototype to itself should throw a recursion error, and
Object.create(fn).toString()should fail with the stack trace a native function produces.
Every failure is recorded with its name, failed toString, failed own property names, failed at too much recursion error and so on, against the prototype and property it came from. A clean browser has an empty list.
Every profile is drawn from one real computer, not from separate listsIn Mosaic: Machines that exist. GPU, screen, CPU and fonts from one real device. Free holds 3 profiles. Pro is $49 a month for any number.
Mosaic is a desktop app
Email yourself the link and open it on your computer.
How it works: Machines that exist
Why the iframe matters
The toString those checks call is not the page's. CreepJS creates a hidden iframe, takes that frame's own Function.prototype.toString, and calls it on the page's functions. A script that blocks the iframe gets that written down too, as an error of its own.
We learned what that costs the hard way. Before our engine, we spoofed fingerprints with a script on stock Chrome, and for a while it hid its patched functions with a rewritten toString that answered from a lookup table of our own functions. In the page, airtight. One realm over, an iframe had its own toString and its own empty table, so the fallback ran and printed each function's real JavaScript body. Measured on one profile with a four-line probe, thirty-three patched functions across Navigator, Screen, canvas, WebGL, audio and client rects gave up their source.
The repair was to make each patch a Proxy whose target is the real native function. V8 prints a proxy over a native as native code in every realm, and the target's name, length and own property names come along. That fixed the iframe test and opened another: CreepJS's proxy checks exist because a Proxy has its own tells. Ours had one more. With toString itself installed as a proxy over the real toString, it recursed until the stack overflowed on a bot-check page, which threw Maximum call stack size exceeded four times and never showed its verdict. The fix was to point that one proxy at a different target, a plain method rather than the real toString, shaped so that 'prototype' in Function.prototype.toString still answers false, as stock Chrome does.
Each of those was a property CreepJS's list checks. The list is long because a spoof has that many ways to differ.
Why an engine change leaves nothing to flag
Every test on the prototype page asks one question: is this the browser's own function? When the value is changed inside the browser, the answer is yes. On Tesseract Chrome, our engine, navigator.hardwareConcurrency is still Chromium's own getter. It returns a different number because the C++ behind it was patched to, and nothing in the page was replaced. Its toString is native because it is native. It has no extra keys, throws the right errors, and gives the same answer in the iframe, because the iframe asks the same C++.
So the prototype page is close to a direct measure of where a spoof lives. Values set in JavaScript show up there, nearly always, unless every one of those tests was anticipated for every patched property in every realm. Values set in the engine don't show up, because there is nothing to show. That is the main reason Mosaic changes fingerprints in the engine and injects no fingerprint script on its own engine: two spoofs of one value, one in C++ and one in JavaScript, would put a lie back on this page.
How we read it in our own catalogue
Our detection catalogue runs this page as an automatic row: pass means no property flagged, and a flagged entry names the exact prototype and property, which is enough to fix it without guessing. That row once came back unread for a reason that had nothing to do with CreepJS. Our extractor was a regular expression inside a template literal, which ate its backslashes, so it matched text the page never prints. Once the extractor parsed, the row read the page's real answer. When a detector looks silent, check the probe before the browser.
To see your own browser's values, the fingerprint checker shows what a page can read. How each Mosaic profile's values are drawn from one real device, instead of being patched one at a time, is on real fingerprints, and what an anti-detect browser is covers the difference between spoofing in the page and changing the browser. Our engine itself was announced in introducing Mosaic.



