Skip to content
On this page

Native function integrity checking is the practice of verifying that a browser's built-in functions and property accessors are still the ones the engine shipped, rather than JavaScript replacements. Because almost every runtime modification technique works by substituting a function or an accessor, proving that the built-ins are untouched is a general-purpose way to detect modification without knowing what was modified. The checks are cheap, deterministic, and hard to satisfy from inside JavaScript, which is why they appear in essentially every serious detection script.

Quick facts

What it verifiesBuilt-in functions and getters are engine-owned, not JavaScript replacements
Primary probeFunction.prototype.toString must report "[native code]"
Structural probeAccessors must live on the prototype, not as own data properties on the instance
Behavioural probeNative methods reject a foreign receiver with a TypeError (brand check)
TrapA nonexistent property must be undefined - a broad Proxy returns a value and fails

The four layers of the check

Integrity checking is layered, and each layer catches modifications that survive the one before it.

1. Serialisation. Function.prototype.toString.call(fn) on a genuine built-in returns function name() { [native code] }. A JavaScript replacement returns its own source. This is the oldest check in the family and is covered in detail in how toString reveals hooked functions.

2. Descriptor shape and location. Most navigator properties are accessors defined on Navigator.prototype, not data properties on the instance. Object.getOwnPropertyDescriptor(navigator, 'userAgent') on an unmodified browser returns undefined, because the property is not an own property of the object at all - it is inherited. A replacement assigned directly to navigator creates an own data property with value and writable keys, which is structurally the wrong shape in the wrong place.

3. Brand checks. Native methods and getters validate their receiver in C++ before doing anything. Calling Object.getOwnPropertyDescriptor(Navigator.prototype, 'userAgent').get.call({}) on a real browser throws a TypeError - the engine refuses a plain object that is not a real Navigator. A JavaScript replacement usually has no such check and cheerfully returns a string, which is a positive signal that the getter is not native.

4. Honeypots. Reading a property that has never existed - a made-up name on navigator - must return undefined. A broadly written Proxy that intercepts every get in order to serve modified values will also answer for names nobody defined, which is a contradiction no real browser produces.

Why the checks compose so well

Each layer independently forces the modification to become more elaborate, and each elaboration creates new surface for the next layer.

Assigning a value directly to navigator.userAgent fails the descriptor check. Moving to Object.defineProperty with a getter fixes the location but fails the toString check, because the getter is a JavaScript function. Patching Function.prototype.toString to lie about that getter fixes the serialisation but means toString itself is now a JavaScript function - so Function.prototype.toString.call(Function.prototype.toString) becomes the contradiction. Reaching for a Proxy to handle the whole object generically fixes the recursion but answers for honeypot names and changes the brand-check behaviour.

There is no fixed point inside JavaScript. The layer doing the modifying is always itself made of JavaScript, and JavaScript is exactly what these checks are able to distinguish from engine code. This is a structural result, not a matter of implementation quality.

What this implies for tooling choices

The practical conclusion is that integrity checks partition tooling into two categories rather than ranking it on a scale. Anything that modifies a running browser from page-world or extension JavaScript is in principle observable, because the modification leaves JavaScript objects where engine objects should be. Anything that changes the value inside the engine before it is ever exposed is not, because there is no replacement to find - the native function is still native and simply returns a different value.

That is why patched browser builds and engine forks behave categorically differently from injection scripts, and it connects directly to realm coherence detection: engine-level changes propagate to every realm automatically, while injected changes must be re-applied per realm by a layer that integrity checks can see.

For teams whose goal is collecting public web data rather than maintaining a browser, this is a strong argument for not owning the problem at all. A managed web data API runs real engines whose built-ins are genuinely untouched, so integrity checks pass because there is nothing to detect rather than because something was hidden well.

Code example

javascript
// The four integrity layers a detection script runs on navigator.userAgent.

// 1. Serialisation: the getter must report as engine code
const desc = Object.getOwnPropertyDescriptor(Navigator.prototype, 'userAgent');
const nativeToString = /\{\s*\[native code\]\s*\}/.test(
  Function.prototype.toString.call(desc.get)
);

// ...and toString itself must be native, or layer 1 is being answered by a lie
const toStringIsNative = /\{\s*\[native code\]\s*\}/.test(
  Function.prototype.toString.call(Function.prototype.toString)
);

// 2. Location: userAgent is inherited, so it is NOT an own property of navigator
const notAnOwnProp = Object.getOwnPropertyDescriptor(navigator, 'userAgent') === undefined;

// 3. Brand check: a native getter refuses a receiver that is not a real Navigator
let brandEnforced = false;
try { desc.get.call({}); } catch (e) { brandEnforced = e instanceof TypeError; }

// 4. Honeypot: a name that never existed must read as undefined
const honeypotClean = navigator.__nonexistent_property_probe__ === undefined;

console.log({ nativeToString, toStringIsNative, notAnOwnProp, brandEnforced, honeypotClean });
// All five true on an unmodified browser. Each false narrows down the technique used.

Next in Coherence and lie detection · 5 of 8

Some values cannot be patched at all. They are compiled in.

What Is Engine/OS Oracle Fingerprinting?

Related terms

Concept map

Concept map

How Native Function Integrity Checking connects

The terms most directly tied to this one. Hover a node to see its neighbours, click to preview, drag to rearrange.

0 terms · 0 connections
You are here · Anti-Bot
Building map…

Frequently asked questions

Why does Object.getOwnPropertyDescriptor(navigator, "userAgent") return undefined on a real browser?

Because userAgent is not an own property of the navigator instance. It is an accessor defined on Navigator.prototype and reached through the prototype chain. Code that assigns a replacement directly onto navigator creates an own data property that should not exist, so the presence of a descriptor is itself the anomaly.

What is a brand check and why can JavaScript not reproduce one?

A brand check is the receiver validation a native method performs in C++ before running: it confirms the object it was called on is genuinely an instance of the right internal type, and throws a TypeError otherwise. JavaScript replacements can imitate this by inspecting the receiver manually, but doing so convincingly across every method is laborious, and the imitation still fails the toString and descriptor layers.

Is there any way to pass all of these checks from inside JavaScript?

Not in general. Every layer of modification is itself written in JavaScript, and these checks are specifically able to distinguish JavaScript objects from engine objects. Improving one layer moves the observable rather than removing it. Passing the checks genuinely requires the built-ins to be real, which means changing behaviour inside the engine rather than in front of it.

Last updated: 2026-07-30 · Facts last verified: 2026-07-30