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.

Try it first. The home page runs every live check described here against your own browser and shows the evidence. The parser runs the string-only checks against anything you paste.

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.

EnginePresent 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.maxTouchPoints is 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-series on Macs, Adreno or Mali on 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/ and Chrome/ in one string.

Where this fails

Consistency is not proof. Every check here detects contradiction. A determined spoofer who changes the string, the Client Hints, 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.