Warum der User Agent eingefroren wurde
Der User-Agent-String war zu einem Fossilienfund geworden. Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36 behauptet, Mozilla, Safari und KHTML zu sein — nichts davon stimmt, aus Kompatibilitätsgründen, die in die 1990er zurückreichen.
Er war außerdem eine passive Fingerprinting-Fläche: Jede Anfrage funkte OS-Version, CPU-Architektur und den exakten Browser-Build an jeden Dritten auf der Seite, ob das jemand brauchte oder nicht.
Googles Antwort waren die User-Agent Client Hints. Den alten String gibt es weiterhin, er wird aber schrittweise eingefroren — beachte, dass Windows NT 10.0 inzwischen auch von Windows 11 gemeldet wird und die Felder der Nebenversion auf 0.0.0 festgenagelt sind. Die echten Details sind auf eine neue Fläche gewandert, aufgeteilt in zwei Stufen danach, wie identifizierend sie sind.
Niedrige und hohe Entropie
Hints mit niedriger Entropie werden ungefragt bei jeder Anfrage mitgeschickt, weil sie wenig verraten:
Sec-CH-UA— die Markenliste, z. B."Chromium";v="147", "Not-A.Brand";v="8", "Google Chrome";v="147"Sec-CH-UA-Mobile—?0oder?1Sec-CH-UA-Platform—"Windows","macOS","Linux"
Hints mit hoher Entropie bleiben zurück, bis ein Server sie über einen Accept-CH-Antwortheader anfordert oder bis ein Skript danach fragt:
architecture—x86,armbitness—64model— das Gerätemodell, vor allem unter Android aussagekräftigplatformVersion— die OS-Version und das Feld, das Windows 10 von 11 unterscheidetfullVersionList— vollständige Markenversionen, z. B.147.0.7727.56wow64— ob ein 32-Bit-Build unter 64-Bit-Windows läuft
Diese Schranke war als Datenschutzkontrolle gedacht. In der Praxis händigt Chrome in First-Party-Kontexten alles Angeforderte ohne Nachfrage und ohne sichtbares Signal aus, sodass ein Fingerprinting-Skript einfach getHighEntropyValues() mit sämtlichen Feldnamen aufruft und alles bekommt.
Wo Profile sich selbst widersprechen
Drei Fehler tauchen immer wieder auf, und alle drei sind mit einer Zeile zu erkennen.
Der String wurde geändert, das Objekt nicht
Der mit Abstand häufigste. Ein Werkzeug schreibt navigator.userAgent so um, dass es Chrome 147 auf macOS behauptet, und lässt navigator.userAgentData weiter Chrome 131 unter Windows melden. Die Prüfung:
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 oder Windows 11
Da der UA-String für beide Windows NT 10.0 meldet, lassen sie sich nur über platformVersion unterscheiden: Werte unter 13 bedeuten Windows 10, 13 und darüber Windows 11. Ein Profil, das in jeder Marketing-Hinsicht Windows 11 behauptet, aber platformVersion: "10.0.0" zurückgibt, behauptet dort, wo es zählt, Windows 10 — und sein Schriftartensatz wird als Nächstes nach Segoe UI Variable gefragt.
Die Markenliste stimmt nicht
Der GREASE-Eintrag — die absichtlich erfundene Marke wie "Not-A.Brand" — existiert, damit Server die Liste nicht fest verdrahten. Sein genauer Text und seine Position ändern sich zwischen Chrome-Versionen, und die Zeichensetzung hat sich über die Zeit gewandelt: Not:A-Brand, Not.A/Brand, Not-A.Brand, mit Version 8, 24 oder 99 je nach Release. Eine aus einem älteren Build kopierte Markenliste ist ein Versionswiderspruch, der offen neben einem neueren User Agent steht.
Das Worker-Problem, schon wieder
navigator.userAgentData existiert auch in Web Workern und Service Workern. Diese Scopes baut die Engine auf eigenen Threads — und wie im CDP-Artikel beschrieben, erreicht ein Override auf Seitenebene sie nicht.
// 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' })));
Jede Abweichung hier ist nicht bloß verdächtig, sondern eindeutig. Es gibt keine Browserkonfiguration, in der Fenster und Worker legitim unterschiedliche Markenlisten melden, weil beide dieselbe Quelle lesen.
Das ist derselbe strukturelle Punkt, der sich durch all diese Artikel zieht, und er sei noch einmal klar gesagt: Ein Wert, über den die Engine entscheidet, lässt sich nur in der Engine überzeugend ändern.
Header und JavaScript müssen auch zusammenpassen
UA-CH existiert an zwei Orten gleichzeitig — in HTTP-Headern und in einem JavaScript-Objekt — und beide werden von verschiedenen Teilen des Browsers erzeugt. Ein Werkzeug, das die eine Seite patcht und die andere nicht, erzeugt einen Widerspruch, der im Serverlog sichtbar ist, ganz ohne clientseitiges Skript.
Die Paare, die übereinstimmen müssen:
| Kopfzeile | JavaScript |
|---|---|
Sec-CH-UA | userAgentData.brands |
Sec-CH-UA-Platform | userAgentData.platform |
Sec-CH-UA-Mobile | userAgentData.mobile |
Sec-CH-UA-Platform-Version | platformVersion |
Sec-CH-UA-Full-Version-List | fullVersionList |
User-Agent | navigator.userAgent |
Sechs Paare, und das letzte bekommt jeder hin. Eine nützliche Disziplin ist, alle aus einem einzigen Profildatensatz abzuleiten, statt sie als getrennte Felder zu konfigurieren — dasselbe Argument wie im Konsistenz-Artikel, auf eine konkrete Fläche angewandt.
Ein Profil prüfen
Führe das in dem Profil aus, das du prüfen willst, und lies jede Zeile gegen den User Agent darüber:
(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);
})();
Vier Fragen an die Ausgabe:
- Stimmt die Plattform im Objekt mit der Plattform im String überein?
- Stimmt die Hauptversion in
fullVersionListmit demChrome/NNNim String überein? - Passt
platformVersionzum Betriebssystem, das du behaupten willst — 13 oder höher für Windows 11? - Liefert ein Worker dieselben Antworten?
browserleaks.com/client-hints zeigt Header- und JavaScript-Seite nebeneinander, was den Vergleich der sechs Paare auf einen Blick erlaubt. Das ist der schnellste Weg, einen halb angewandten Spoof zu finden.