Por qué se congeló el user agent
La cadena del user agent se había convertido en un registro fósil. Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36 dice ser Mozilla, Safari y KHTML, y ninguna de las tres es cierta, por razones de compatibilidad que vienen de los años noventa.
Era además una superficie de fingerprinting pasivo: cada petición emitía la versión del sistema operativo, la arquitectura de CPU y la build exacta del navegador a todos los terceros de la página, los necesitara alguien o no.
La respuesta de Google fueron los User-Agent Client Hints. La cadena antigua sigue existiendo pero se congela de forma progresiva — fíjate en que Windows NT 10.0 ahora lo informa también Windows 11, y los campos de versión menor están fijados a 0.0.0. El detalle real se mudó a una superficie nueva, dividida en dos niveles según lo identificativa que sea.
Baja entropía y alta entropía
Los hints de baja entropía se envían en cada petición sin que nadie los pida, porque revelan poco:
Sec-CH-UA— la lista de marcas, por ejemplo"Chromium";v="147", "Not-A.Brand";v="8", "Google Chrome";v="147"Sec-CH-UA-Mobile—?0o?1Sec-CH-UA-Platform—"Windows","macOS","Linux"
Los hints de alta entropía se retienen hasta que un servidor los solicita con una cabecera de respuesta Accept-CH, o hasta que un script los pide:
architecture—x86,armbitness—64model— el modelo del dispositivo, relevante sobre todo en AndroidplatformVersion— la versión del sistema operativo, y el campo que distingue Windows 10 de Windows 11fullVersionList— versiones completas de las marcas, por ejemplo147.0.7727.56wow64— si se está ejecutando una build de 32 bits sobre Windows de 64 bits
Ese filtro se diseñó como control de privacidad. En la práctica, en contextos de primera parte Chrome entrega todo lo solicitado sin aviso ni señal visible, así que a un script de fingerprinting le basta con llamar a getHighEntropyValues() con todos los nombres de campo para obtenerlos todos.
Dónde se contradicen los perfiles
Tres fallos aparecen una y otra vez, y los tres se detectan con una línea.
Se cambió la cadena y no el objeto
El más común con diferencia. Una herramienta reescribe navigator.userAgent para decir Chrome 147 en macOS y deja navigator.userAgentData informando Chrome 131 en Windows. La comprobación:
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
Como la cadena UA informa Windows NT 10.0 para ambos, la única forma de distinguirlos es platformVersion: valores por debajo de 13 significan Windows 10, y 13 o superior, Windows 11. Un perfil que dice ser Windows 11 en todo lo aparente pero devuelve platformVersion: "10.0.0" está diciendo Windows 10 donde importa — y a su conjunto de fuentes le preguntarán por Segoe UI Variable a continuación.
La lista de marcas está mal
La entrada GREASE — la marca deliberadamente falsa, como "Not-A.Brand" — existe para que los servidores no fijen la lista a mano. Su texto exacto y su posición cambian entre versiones de Chrome, y la puntuación ha variado con el tiempo: Not:A-Brand, Not.A/Brand, Not-A.Brand, con la versión en 8, 24 o 99 según el lanzamiento. Una lista de marcas copiada de una build antigua es un desajuste de versión a plena vista junto a un user agent más nuevo.
El problema de los workers, otra vez
navigator.userAgentData existe dentro de los Web Workers y los Service Workers. Esos ámbitos los construye el motor en sus propios hilos y, como se vio en el artículo sobre CDP, un override a nivel de página no llega hasta ellos.
// 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' })));
Cualquier desacuerdo aquí es concluyente, no solo sospechoso. No hay configuración de navegador en la que la ventana y un worker informen legítimamente listas de marcas distintas, porque ambos leen la misma fuente.
Es el mismo punto estructural que se repite en todos estos artículos, y conviene decirlo una vez más con claridad: un valor que decide el motor solo puede cambiarse de forma convincente en el motor.
Las cabeceras y JavaScript también tienen que cuadrar
UA-CH existe en dos sitios a la vez — cabeceras HTTP y un objeto JavaScript — y los producen partes distintas del navegador. Una herramienta que parchea uno y no el otro crea una contradicción visible en un log de servidor sin ningún script en el cliente.
Los pares que deben coincidir:
| Cabecera | 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 |
Seis pares, y el último lo acierta todo el mundo. Una disciplina útil es derivarlos todos de un único registro de perfil en lugar de configurarlos como campos sueltos — el mismo argumento de el artículo sobre coherencia, aplicado a una superficie concreta.
Auditar un perfil
Ejecuta esto en el perfil que quieras comprobar y lee cada línea contra el user agent que tiene encima:
(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);
})();
Cuatro preguntas que hacerle a la salida:
- ¿La plataforma del objeto coincide con la plataforma de la cadena?
- ¿La versión mayor de
fullVersionListcoincide con elChrome/NNNde la cadena? - ¿
platformVersiones coherente con el sistema operativo que quieres declarar — 13 o superior para Windows 11? - ¿Un worker devuelve las mismas respuestas?
browserleaks.com/client-hints muestra a la vez el lado de las cabeceras y el de JavaScript, lo que hace fácil comparar de un vistazo los seis pares anteriores. Es la forma más rápida de encontrar un spoof aplicado a medias.