· 3 min
Native vs JavaScript Fingerprint Spoofing
Why fingerprint spoofing by JavaScript injection leaks through toString, descriptors, workers, iframes and Proxy, and why engine-level patches hold up.
There are two ways to change what a browser reports. You can inject JavaScript into each page and override APIs. Or you can change the browser engine so that the APIs return other values natively. The first is easy to build. The second is hard to detect.
How JavaScript injection works
An extension or a launch script runs before the page and redefines APIs. For example, it replaces the navigator.hardwareConcurrency getter or wraps HTMLCanvasElement.prototype.toDataURL to add noise. The page then reads the new values.
Where JavaScript injection leaks
| Check | What the site sees |
|---|---|
Function.prototype.toString |
A native function returns function x() { [native code] }. A replaced one returns its source, unless the tool also patches toString, which is a second patch to detect. |
| Property descriptors | Native getters live on the prototype. A patch on the instance, or a descriptor with the wrong configurable or enumerable flags, is visible with Object.getOwnPropertyDescriptor. |
| Function properties | name, length and the presence of a prototype property differ between native and script functions. |
| Proxy wrappers | A Proxy hides the source, but stack traces, recursion limits and some type checks still show an extra layer. |
| Workers | A Web Worker, Shared Worker or Service Worker starts a new scope. Injected scripts often do not run there, so the worker reports the real values. |
| Iframes | A new iframe gets a fresh window. A site can read APIs from it before the injected script runs, or call clean functions from it on the main page objects. |
| Timing | Wrapped functions are slower. Repeated calls can show the overhead. |
Each fix adds more code, and each new piece of code is one more surface to check. The site only needs one contradiction.
How engine-level patches work
With native spoofing, the change is in the browser source, in C++. The API is still the original native function. It returns a different value because the engine computes a different value. So:
toStringreturns[native code]because the function is native.- Descriptors and prototypes are the original ones.
- Workers, iframes and the main page share the same engine, so all scopes agree.
- Nothing runs in the page, so there is nothing for a page script to find.
The cost is work. The vendor must maintain a browser fork and follow upstream releases. An old engine version is a signal on its own, so update cadence matters.
Engine scores in our data
| Browser | Approach | Engine score |
|---|---|---|
| Donut Browser | Chromium fork, spoofing in C++ | 9 |
| Kameleo | Two native engines; Chroma follows Chrome within 5 days | 9 |
| Multilogin | Mimic updated every 2–6 weeks; Stealthfox frozen | 9 |
| Camoufox | Firefox fork, injection in C++; beta builds | 8 |
| Octo Browser | Chromium; cadence not published | 8 |
Most other vendors do not publish engine details or update cadence. We score them 6 or 7. That is not proof of JavaScript injection. It means we cannot confirm the method.
Donut Browser states it plainly: fingerprints are patched in C++, with no JS hooks to detect. Combined with unlimited free local profiles, that is a large part of why it ranks first.
Check it yourself
Our fingerprint scan runs the native-integrity checks above, and compares workers and iframes with the main page. For the bigger picture, read how websites detect antidetect browsers.