Detecting a fake user agent
The header is a claim, not evidence. Here is how to cross-examine it — and where every method runs out of road.
The core idea
You cannot verify a user agent by looking at it. Anyone can send any string, and the header carries no signature. What you can do is compare the claim against things the browser reveals involuntarily — behaviour that comes from the engine, the operating system and the hardware, and that a user agent override does not touch.
A genuine user agent agrees with all of them. A spoofed one contradicts at least one, because a switcher rewrites a string but cannot rewrite a rendering engine.
1. Engine-exclusive APIs — the strongest signal
Each engine ships APIs the others do not have. These are not preferences; they are the engine.
| Engine | Present only there |
|---|---|
| Blink (Chromium) | window.chrome, navigator.userAgentData, showOpenFilePicker, PressureObserver |
| Gecko (Firefox) | window.mozInnerScreenX, navigator.buildID, navigator.oscpu, CSS.supports('-moz-appearance','none') |
| WebKit (Safari) | window.GestureEvent, ApplePaySession, navigator.standalone, webkitConvertPointFromNodeToPage |
A string claiming Firefox from a browser exposing window.chrome and navigator.userAgentData is Chromium wearing a costume. Score several of these rather than trusting one — InstallTrigger was the classic Firefox test for a decade and Firefox 128 removed it, quietly breaking a great deal of detection code.
There is also a free signal in error objects. V8 formats stack traces as at fn (file:1:1); SpiderMonkey and JavaScriptCore both use fn@file:1:1. That one line separates Chromium from everything else without touching any API.
2. Client Hints — what most switchers forget
On Chromium, navigator.userAgentData and the Sec-CH-UA-* headers are generated independently of the user agent string. Override the string with a command-line flag or a naive extension and the hints keep reporting the truth:
navigator.userAgent // "...Macintosh; Intel Mac OS X..." ← the lie
navigator.userAgentData.platform // "Windows" ← the truth
Chrome's own DevTools keeps the two in sync, so this catches sloppy spoofing rather than careful spoofing. It remains one of the highest-yield checks in practice.
3. navigator.platform and navigator.vendor
Both are deprecated, both are still present everywhere, and both are frequently left untouched. navigator.platform returns Win32, MacIntel, Linux x86_64 or iPhone. navigator.vendor returns Google Inc. on Chromium, Apple Computer, Inc. on Safari and an empty string on Firefox. A UA claiming Windows next to MacIntel is a contradiction the browser did not intend to reveal.
4. Hardware that cannot be talked out of
- Touch points.
navigator.maxTouchPointsis 0 on a desktop and 5 on a phone. A UA claiming an iPhone with zero touch points is not an iPhone. - Screen geometry. No phone has a 2560-pixel-wide viewport. No iPhone in support reports a device pixel ratio of 1.
- GPU driver. The WebGL renderer string comes from the operating system's driver:
ANGLE (…Direct3D11…)on Windows,Apple M-serieson Macs,AdrenoorMalion Android. A Windows claim next to an Adreno renderer is a contradiction.
5. Feature era versus claimed version
This one catches understated versions, which spoofers use to look like an older, less-restricted client. Every JavaScript feature has a version it first shipped in. Test for a handful and you can bound the real version from below:
Array.prototype.at → Chrome 92+, Firefox 90+, Safari 15.4+
Object.hasOwn → Chrome 93+, Firefox 92+, Safari 15.4+
structuredClone → Chrome 98+, Firefox 94+, Safari 15.4+
Array.prototype.toSorted→ Chrome 110+, Firefox 115+, Safari 16+
A user agent claiming Chrome 70 from a browser that supports toSorted is understating itself by forty releases.
6. Checks on the string alone
Where no JavaScript environment exists — a server log, a pasted string — internal consistency is still testable. Some combinations simply cannot occur:
- Safari above 5.1 on Windows. Apple discontinued Safari for Windows in 2012 at 5.1.7.
- Chrome on iOS without
CriOS. Apple requires WebKit, and every iOS browser carries its own token. - Chromium with a WebKit build other than 537.36. That number has been frozen since 2013.
- A Chromium fork with no
Chrome/token. Edge, Opera and Samsung Internet always keep it. - Android and Chrome from different eras. Android 16 with Chrome 60 was never a shipping combination.
- Rival tokens together.
Firefox/andChrome/in one string.
Where this fails
navigator.platform, the vendor, the touch points and the screen dimensions together produces a claim with no contradictions in it — and passes. Passing means "nothing here disagrees", never "this is genuine".
Three specific traps are worth naming, because each produces a false accusation:
- iPad desktop mode. Since iPadOS 13, Safari sends a Macintosh user agent by design while still reporting five touch points. That looks exactly like spoofing and is completely legitimate. Detect it (Macintosh + touch +
GestureEvent) and exempt it. - Privacy browsers. Brave and Arc deliberately send an unmodified Chrome string; the Tor Browser sends an identical Windows string on every platform. These are anti-fingerprinting measures, not deception, and the Tor case is internally consistent by design.
- Firefox's
resistFingerprinting. It spoofs the platform, the timezone and the screen size together, so it stays self-consistent while telling you almost nothing true.
What to do with the answer
Use it for analytics hygiene, bot triage and debugging — segmenting traffic, sanity-checking a device matrix, working out why a bug reproduces only for one population. Do not use it as an access control. A visitor with a modified user agent is overwhelmingly a developer testing something, a privacy-conscious user, or someone on a corporate browser with a custom token appended. Blocking on this signal costs you real users to catch scrapers who will simply fix their string.
For anything security-critical, verify behaviour instead: rate limits, proof of work, reverse DNS for crawlers, and authentication for anything that matters.
Frequently asked questions
Can a website tell for certain that I changed my user agent?
Often yes, if you changed only the string. Your engine, platform, touch capability, GPU and JavaScript feature set all continue describing your real browser, and a contradiction between those and the string is straightforward to spot. Change everything consistently and detection becomes very difficult — which is what dedicated anti-fingerprinting browsers do.
Does a failed check mean I am doing something wrong?
Not at all. The most common reasons for a contradiction are entirely ordinary: developer tools left in device-emulation mode, a user agent switcher extension, a privacy browser, or an iPad requesting desktop sites. The check reports disagreement between two signals, nothing more.
Why does my Brave browser show as Chrome and still pass?
Because Brave genuinely is Chromium and sends a genuine Chrome user agent on purpose, so its engine, hints and platform all agree with the string. There is no contradiction to find. Brave hides in Chrome's crowd deliberately — a browser with 1% share is far more identifiable than one indistinguishable from the majority.
Can this detect headless browsers and bots?
Partly. Old headless Chrome announced itself in the string with HeadlessChrome, and the parser flags that immediately. Modern automation frameworks remove the token and patch the obvious tells, so the reliable signals move to behaviour — timing, mouse movement, order of requests — rather than anything visible in a user agent.
Is any of this sent to your server?
No. Every check runs in your browser, and the results never leave the page. See the privacy policy.