AntiDetect
Features Technology Pricing Who's It For
Docs Blog FAQ
Download

The User Agent Moved, and Half the Spoofing Did Not Follow

Chrome froze the user agent string and moved the real information into Client Hints. Plenty of setups still rewrite the old string and leave navigator.userAgentData reporting the truth — a contradiction available to any page for the cost of one await.

Why the user agent was frozen

The user agent string had become a fossil record. Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36 claims to be Mozilla, Safari and KHTML, none of which is true, for compatibility reasons dating to the 1990s.

It was also a passive fingerprinting surface: every request broadcast the OS version, the CPU architecture and the exact browser build to every third party on the page, whether or not anyone needed it.

Google's answer was User-Agent Client Hints. The old string still exists but is progressively frozen — note that Windows NT 10.0 is now reported by Windows 11 as well, and the minor version fields are pinned to 0.0.0. The real detail moved to a new surface, split into two tiers by how identifying it is.

Low entropy and high entropy

Low entropy hints are sent on every request without anyone asking, because they reveal little:

High entropy hints are withheld until a server opts in via an Accept-CH response header, or until a script asks for them:

The gating was designed as a privacy control. In practice, for first-party contexts Chrome hands over everything requested with no prompt and no visible signal, so a fingerprinting script simply calls getHighEntropyValues() with every field name and gets them all.

Where profiles contradict themselves

Three failures show up repeatedly, and all three are one line to detect.

The string was changed and the object was not

The single most common. A tool rewrites navigator.userAgent to claim Chrome 147 on macOS, and leaves navigator.userAgentData reporting Chrome 131 on Windows. The check:

const ua = navigator.userAgent;
const d  = await navigator.userAgentData.getHighEntropyValues(
  ['platform','platformVersion','architecture','fullVersionList']);
// ua says "Macintosh; Intel Mac OS X 10_15_7"
// d.platform === "Windows"   →  contradiction, no ambiguity

Windows 10 or Windows 11

Since the UA string reports Windows NT 10.0 for both, the only way to tell them apart is platformVersion: values below 13 mean Windows 10, 13 and above mean Windows 11. A profile that claims Windows 11 in every marketing sense but returns platformVersion: "10.0.0" is claiming Windows 10 where it counts — and its font set will be asked about Segoe UI Variable next.

The brand list is wrong

The GREASE entry — the deliberately fake brand like "Not-A.Brand" — exists to stop servers hard-coding the list. Its exact text and position change between Chrome versions, and the punctuation has varied over time: Not:A-Brand, Not.A/Brand, Not-A.Brand, with the version at 8, 24 or 99 depending on release. A brand list copied from an older build is a version mismatch sitting in plain sight beside a newer user agent.

The worker problem, again

navigator.userAgentData exists inside Web Workers and Service Workers. Those scopes are built by the engine on their own threads, and — as covered in the CDP article — a page-level override does not reach them.

// main thread: brands say 147
// worker: brands say whatever the binary really is
new Worker(URL.createObjectURL(new Blob([`
  navigator.userAgentData.getHighEntropyValues(['fullVersionList','platform'])
    .then(v => postMessage(v))
`], { type: 'text/javascript' })));

Any disagreement here is conclusive rather than suspicious. There is no browser configuration in which the window and a worker legitimately report different brand lists, because both read the same source.

This is the same structural point that keeps recurring across these articles, and it is worth stating once more plainly: a value that is decided by the engine can only be changed convincingly in the engine.

Headers and JavaScript have to agree too

UA-CH exists in two places at once — HTTP headers and a JavaScript object — and they are produced by different parts of the browser. A tool that patches one and not the other creates a contradiction visible in a server log without any client-side script at all.

The pairs that must match:

HeaderJavaScript
Sec-CH-UAuserAgentData.brands
Sec-CH-UA-PlatformuserAgentData.platform
Sec-CH-UA-MobileuserAgentData.mobile
Sec-CH-UA-Platform-VersionplatformVersion
Sec-CH-UA-Full-Version-ListfullVersionList
User-Agentnavigator.userAgent

Six pairs, and the last one is the one everybody gets right. A useful discipline is to derive all of them from a single profile record rather than configuring them as separate fields — the same argument made in the consistency article, applied to one specific surface.

Six HTTP Client Hint headers paired with their JavaScript counterparts, one pair broken
Six pairs produced by different parts of the browser. Patch one side and the other contradicts it — visible in a server log with no client-side script at all.

Auditing a profile

Run this in the profile you want to check and read every line against the user agent above it:

(async () => {
  const d = await navigator.userAgentData.getHighEntropyValues(
    ['architecture','bitness','model','platformVersion','fullVersionList','wow64']);
  console.log(navigator.userAgent);
  console.log(navigator.userAgentData.platform, navigator.userAgentData.mobile);
  console.log(navigator.userAgentData.brands);
  console.log(d);
})();

Four questions to ask of the output:

  1. Does the platform in the object match the platform in the string?
  2. Does the major version in fullVersionList match the Chrome/NNN in the string?
  3. Is platformVersion consistent with the OS you intend to claim — 13 or higher for Windows 11?
  4. Does a worker return the same answers?

browserleaks.com/client-hints shows both the header and JavaScript sides together, which makes the six pairs above easy to compare at a glance. It is the fastest way to find a half-applied spoof.

One version number, everywhere

P8 derives the UA string, the Client Hint headers and userAgentData from the same profile — including inside workers, where injected scripts cannot reach.

Start for Free
All articles Next: Your Font List Is an OS Confession