Perché lo user agent è stato congelato
La stringa dello user agent era diventata un reperto fossile. Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36 dichiara di essere Mozilla, Safari e KHTML, e nessuna delle tre è vera, per ragioni di compatibilità che risalgono agli anni Novanta.
Era anche una superficie di fingerprinting passivo: ogni richiesta trasmetteva la versione del sistema operativo, l'architettura della CPU e la build esatta del browser a ogni terza parte presente nella pagina, che servisse a qualcuno o no.
La risposta di Google sono stati gli User-Agent Client Hints. La vecchia stringa esiste ancora ma viene progressivamente congelata — nota che Windows NT 10.0 ora viene riportato anche da Windows 11, e i campi di versione minore sono fissati a 0.0.0. Il dettaglio vero si è spostato su una nuova superficie, divisa in due livelli a seconda di quanto identifichi.
Bassa entropia e alta entropia
Gli hint a bassa entropia vengono inviati a ogni richiesta senza che nessuno li chieda, perché rivelano poco:
Sec-CH-UA— l'elenco dei brand, ad esempio"Chromium";v="147", "Not-A.Brand";v="8", "Google Chrome";v="147"Sec-CH-UA-Mobile—?0oppure?1Sec-CH-UA-Platform—"Windows","macOS","Linux"
Gli hint ad alta entropia vengono trattenuti finché un server non li richiede con un header di risposta Accept-CH, oppure finché non li chiede uno script:
architecture—x86,armbitness—64model— il modello del dispositivo, significativo soprattutto su AndroidplatformVersion— la versione del sistema operativo, il campo che distingue Windows 10 da Windows 11fullVersionList— le versioni complete dei brand, ad esempio147.0.7727.56wow64— se è in esecuzione una build a 32 bit su Windows a 64 bit
Questo filtro è stato pensato come controllo per la privacy. In pratica, nei contesti di prima parte Chrome consegna tutto ciò che viene richiesto senza alcun prompt e senza alcun segnale visibile, quindi a uno script di fingerprinting basta chiamare getHighEntropyValues() con tutti i nomi dei campi per ottenerli tutti.
Dove i profili si contraddicono
Tre errori ricorrono di continuo, e tutti e tre si rilevano con una riga.
La stringa è stata cambiata e l'oggetto no
Il più comune in assoluto. Uno strumento riscrive navigator.userAgent per dichiarare Chrome 147 su macOS, e lascia navigator.userAgentData a dire Chrome 131 su Windows. Il controllo:
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 o Windows 11
Dato che la stringa UA riporta Windows NT 10.0 per entrambi, l'unico modo per distinguerli è platformVersion: valori sotto 13 indicano Windows 10, 13 e oltre indicano Windows 11. Un profilo che dichiara Windows 11 in ogni senso di facciata ma restituisce platformVersion: "10.0.0" sta dichiarando Windows 10 dove conta — e al suo insieme di font verrà chiesto conto di Segoe UI Variable subito dopo.
L'elenco dei brand è sbagliato
La voce GREASE — il brand deliberatamente falso come "Not-A.Brand" — esiste per impedire ai server di codificare l'elenco a mano. Il suo testo esatto e la sua posizione cambiano fra le versioni di Chrome, e la punteggiatura è variata nel tempo: Not:A-Brand, Not.A/Brand, Not-A.Brand, con la versione a 8, 24 o 99 a seconda del rilascio. Un elenco di brand copiato da una build più vecchia è un'incoerenza di versione in bella vista accanto a uno user agent più recente.
Il problema dei worker, di nuovo
navigator.userAgentData esiste dentro i Web Worker e i Service Worker. Quegli scope sono costruiti dal motore sui loro thread e — come visto nell'articolo su CDP — un override a livello di pagina non li raggiunge.
// 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' })));
Qualsiasi discordanza qui è decisiva, non solo sospetta. Non esiste configurazione del browser in cui la finestra e un worker riportino legittimamente elenchi di brand diversi, perché entrambi leggono la stessa sorgente.
È lo stesso punto strutturale che ritorna in tutti questi articoli, e vale la pena dirlo ancora una volta chiaramente: un valore deciso dal motore può essere cambiato in modo convincente solo nel motore.
Anche header e JavaScript devono concordare
UA-CH esiste in due posti contemporaneamente — negli header HTTP e in un oggetto JavaScript — e sono prodotti da parti diverse del browser. Uno strumento che corregge l'uno e non l'altro crea una contraddizione visibile in un log del server senza alcuno script lato client.
Le coppie che devono coincidere:
| Intestazione | 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 |
Sei coppie, e l'ultima è quella che tutti azzeccano. Una disciplina utile è ricavarle tutte da un unico record di profilo invece di configurarle come campi separati — lo stesso argomento dell'articolo sulla coerenza, applicato a una superficie specifica.
Fare l'audit di un profilo
Esegui questo nel profilo che vuoi controllare e leggi ogni riga confrontandola con lo user agent sopra di essa:
(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);
})();
Quattro domande da porre all'output:
- La piattaforma nell'oggetto corrisponde a quella nella stringa?
- La versione maggiore in
fullVersionListcorrisponde aChrome/NNNnella stringa? platformVersionè coerente con il sistema operativo che intendi dichiarare — 13 o superiore per Windows 11?- Un worker restituisce le stesse risposte?
browserleaks.com/client-hints mostra insieme il lato header e quello JavaScript, il che rende facile confrontare a colpo d'occhio le sei coppie qui sopra. È il modo più rapido per trovare uno spoof applicato a metà.