AlterAntiX
Back to blog
Technical11 min read

User Agent Spoofing: Why Changing the String Doesn't Fool Anyone Anymore

Changing the user agent alone has not fooled anyone in years, long before you started running multiple accounts. This piece shows the 4 cross-checks that expose a fake user agent and why TLS gives the string away before any JavaScript even runs.

Install an extension, pick a different browser from a dropdown, and the site thinks you are on a Mac. That was true for about a decade, and it stopped being true a long time ago. Here are the four cross-checks that expose a swapped user agent, and why the one that matters most has already answered before your first script runs.

The technique every old tutorial still teaches

The user agent is a single header the browser attaches to every request, describing itself: browser, version, engine, operating system. It is a self-declaration, and nobody ever verified it, which is exactly why it became the first thing people learned to change. For years, changing it worked, because sites had nothing else to compare it against.

Two things ended that era. The first is that browsers themselves reduced how much the string says. Chrome froze most of the detail years ago; the operating system version is now generic, the minor version numbers are zeros, and the string that used to carry a precise machine description now carries a rough category. The second is that everything the string used to be trusted for moved elsewhere, into places a header cannot reach.

So the modern situation is not that the user agent is checked more strictly. It is that the user agent has become a claim that four other systems can independently verify, and nobody bothers checking a claim they can just measure.

Does changing the user agent still fool detectors?

Not on its own, and the reason is structural rather than a matter of how good the tool is. A user agent swap changes an assertion. It does not change what the machine does. Every check below reads a behaviour rather than an assertion, which is why they cannot be satisfied by editing a string — and why they contradict each other the moment the string moves and the behaviour does not.

What follows is not a ranking of severity. It is four independent surfaces, and a serious risk system reads all four and compares them. One contradiction is an anomaly. Two is a conclusion.

Worth saying before the criticism: changing the user agent is a perfectly good tool for the job it was designed for. Testing how a page renders on a phone, forcing a site to serve its mobile layout, checking that a legacy browser branch still works — a switcher extension does all of that well, and none of it involves convincing anyone of anything. The problem starts when a technique built for testing gets sold as identity separation. Those are different goals, and only one of them is contested by the other side.

Cross-check 1 — the rest of the navigator object

The user agent is one property among dozens that describe the same machine, and the others are read from JavaScript rather than from a header. The first and cheapest comparison is navigator.platform against the operating system named in the string: a user agent claiming Windows alongside a platform value reporting a Mac is a contradiction that costs a detector one line of code.

The neighbouring values matter for the same reason. navigator.hardwareConcurrency reports CPU cores, deviceMemory reports memory in a small set of permitted values, screen width, height and pixel ratio describe a display. None of these move when the user agent moves. A profile declaring an iPhone while reporting a two-thousand-pixel-wide desktop screen, sixteen cores and thirty two gigabytes of memory has described a device that does not exist.

The trap here is that fixing them looks easy and is not. Every value you set has to remain in the range real hardware reports. Memory only comes in specific values the API is allowed to expose; a screen has to be a size displays are actually manufactured in; core counts have plausible bounds. A profile with values chosen freely is more identifiable than one that never changed anything, because it lands in a region of the space where no real machines live.

Cross-check 2 — Client Hints, the part the string does not cover

This is the check most tutorials have never heard of, and it is the one that catches extension-level spoofing almost every time.

As the user agent string was being reduced, Chromium introduced a replacement designed to deliver the same information in a structured, negotiable form: User-Agent Client Hints. Three of them are low entropy and go out on every request without anybody asking — the brand and significant version, a mobile flag, and the platform. The rest are high entropy: platform version, architecture, bitness, device model, the full version list. Those only ship when the server explicitly requests them with an Accept-CH response header, or when the page reads them through getHighEntropyValues in JavaScript.

The consequence is direct. A tool that rewrites the user agent header and stops there leaves a second, parallel description of the machine going out on every single request, still describing the real one. The site does not need to be clever; it just reads both and notices they disagree.

The scope trap almost nobody mentions
Even a tool that patches Client Hints in JavaScript often patches them only in the page window. Iframes, web workers, service workers and the headers attached to background fetch requests are separate execution contexts, and a patch applied to one window does not reach them. A site can read the spoofed value in the page and the real one from a worker, in the same session. Handling this properly means overriding at the browser level rather than inside the page.

Cross-check 3 — the rendering layer

The third surface does not ask the browser what it is. It asks it to draw, and then reads the result. Canvas output, WebGL image output, the unmasked WebGL vendor and renderer strings, and the list of fonts the system can resolve all describe the machine underneath rather than the machine being claimed.

The renderer string is the most direct of these. A real Windows machine reports a Direct3D-backed ANGLE string, a Linux machine reports an OpenGL string, a Mac reports an Apple string that mentions Metal. That value describes the graphics stack, and the graphics stack belongs to the operating system. A user agent claiming Windows over a renderer describing another platform is not a subtle mismatch, and it does not need statistical analysis to spot.

Fonts work the same way, from the other direction. Every operating system ships a characteristic set, and a page can test which ones resolve by measuring the width of a rendered string. A profile claiming macOS on a machine that cannot resolve a single Apple system font has answered the question by omission. Our piece on canvas and WebGL goes through this layer in full, including why naive noise makes it worse.

Cross-check 4 — TLS, which answers before JavaScript exists

The fourth check happens before the page. Every HTTPS connection opens with a TLS handshake, and the first packet — the ClientHello — carries the cipher suites the client supports and their order, the extensions it offers and their order, the elliptic curves it accepts, and the application protocol it prefers. That combination is characteristic of a specific browser built from a specific engine version. JA3 and JA4 are the standard ways of hashing it into a fingerprint.

Two properties make this the hardest layer to fake. It is assembled by the TLS library of the runtime, so no amount of JavaScript can rewrite it. And it goes out before the first byte of HTML, which means the server already has it when your user agent header arrives — the contradiction is available at the moment the request lands, not after analysis.

This is where the whole article converges. You can pass the navigator check, the Client Hints check and the rendering check, and still be contradicted by a handshake that says a different browser sent this. Worse, a partial fix creates its own problem: a profile whose user agent says one Chrome version while the TLS imitates another is describing a browser that has never shipped. We covered that layer in depth separately — this article is one layer above it, and the two only work together.

What has to change together with the user agent

Read as a checklist, the four surfaces produce a short list of things that have to move as a set. Anything left behind becomes the contradiction.

SignalWhat a UA swap alone doesWhat actually has to happen
User agent headerChangedChanged, and pinned to a version the rest of the stack can support
Client Hints (low entropy, every request)Usually untouchedRewritten at browser level so every context agrees, not just the page window
navigator.platform, cores, memory, screenUntouchedSet to values consistent with the declared system and inside real hardware ranges
WebGL vendor and rendererUntouchedA graphics stack that belongs to the declared operating system
Font listUntouchedA font set characteristic of the declared system
TLS ClientHello (JA3 / JA4)Untouched — JavaScript cannot reach itA handshake matching the exact browser version the user agent claims

Reading that table top to bottom explains why extensions cannot solve this and why the problem belongs to the browser rather than to a page-level tool. Rows one and two can be reached from inside a page, with the scope limitations described above. Rows three to five require control over the runtime. Row six requires control over the network stack, which is outside the browser entirely.

There is also a rule of thumb hidden in the table, and it inverts the usual instinct. The safest user agent to declare is not the newest or the rarest — it is the one every other row can actually support. A profile is better off claiming a Chrome version its TLS layer reproduces exactly and its rendering stack can plausibly belong to, even if that version is a few releases behind, than claiming the latest release and contradicting itself twice to get there. Consistency is worth more than currency here, and it is the opposite of what most configuration screens encourage.

How AlterAntiX keeps it coherent

AlterAntiX treats these as one object rather than six settings. Three parts are worth describing concretely, because they are checkable claims rather than adjectives.

Client Hints are set at the browser level, not injected into the page. The user agent metadata is applied through the browser control protocol, which propagates it to every execution context — page, iframes, workers, service workers and outgoing fetch headers — instead of only the window a page script can see. That is what closes the scope trap described earlier.

A coherence validator runs against the profile and reports contradictions. It compares the user agent against the platform, checks the mobile marker against the declared device class, verifies the WebGL vendor and renderer belong to the same family, confirms an Apple GPU is not claimed on a non-Apple system, checks that the first declared language matches the locale, and validates screen, pixel ratio, core count and memory against plausible ranges. The score is an open formula: the profile starts at 100 and loses 10 points for each contradiction found, plus a proxy penalty — 30 for a dead proxy, 20 for a missing one, 10 for one never tested, 5 for no proxy at all.

The Chrome version picker tells you where TLS coverage is exact. The bundled TLS impersonation ships a finite set of presets, and choosing a Chrome version without an exact match means the handshake imitates a neighbouring version while the string says something else. Rather than hiding that, the version selector marks each option as an exact match, a close match or no close match, and the profile default sits on a version where the two layers agree. Being told which choice is safe is more useful than being offered every version as if they were equivalent.

You can check all of this from outside the product. Open the fingerprint checker in whatever you use today and compare the declared user agent against the platform, the Client Hints, the WebGL renderer and the TLS fingerprint on the same screen. If any two disagree, you found the contradiction before a platform did. AlterAntiX is a free desktop download for Windows and Linux if you want to compare a coherent profile against your current setup.

Take this away
  • The user agent is a claim. Every check that matters reads a behaviour instead, so a swapped string just creates a contradiction.
  • Client Hints ship a parallel description of the machine on every request — three of them without the server even asking.
  • A patch applied only to the page window leaves iframes, workers and background requests reporting the truth.
  • The rendering layer answers by drawing: WebGL renderer and font list describe the real operating system.
  • TLS answers first of all, before any JavaScript, and JavaScript cannot reach it.
  • Coherence is the goal, not concealment: everything has to describe one plausible machine, including the browser version.

Frequently asked questions

Is a user-agent switcher extension useful for anything?

It is useful for testing responsive layouts and forcing the mobile version of a site. It is not useful for separating account identities. The extension swaps the string and, depending on the implementation, the Client Hints — and touches nothing in the rendering layer or the network layer, which are precisely the ones that give you away.

What is the cheapest signal that exposes a fake user agent?

Comparing the operating system declared in the string against what the graphics stack reports through WebGL. A Windows string with a renderer describing another system stack is a direct contradiction, costs one comparison, and has no innocent explanation.

Are Client Hints always sent?

Three of them are: brand and significant version, the mobile flag and the platform go out on every request in Chromium browsers. The rest — platform version, architecture, model, full version list — are high entropy and only ship when the server asks for them with Accept-CH or when the page calls getHighEntropyValues in JavaScript.

Why does TLS give the string away before JavaScript?

Because the TLS handshake happens before the first line of HTML arrives. The ClientHello carries the cipher list, the extension order and the ALPN, and that combination is characteristic of each browser and version. When JA3 or JA4 says one thing and the user agent says another, the server already had both answers before any script ran.

Is switching the user agent to a newer version safer?

Only if everything else follows. A Chrome version with no matching TLS preset makes the network layer imitate a neighbouring version, and then the string and the handshake diverge. Prefer the versions your tool has exact TLS coverage for over the newest one that exists.

What does AlterAntiX do differently here?

It treats user agent, platform, Client Hints, WebGL, screen, language and TLS preset as one set that has to agree, with a coherence validator that surfaces the contradiction before the profile launches, and a Chrome version picker that flags which versions have an exact TLS match.

Fingerprint and TLS, actually aligned

AlterAntiX matches the browser fingerprint with the TLS handshake on the same Chrome version. Download and test it yourself.

Download AlterAntiX