Parches JS de navegador: qué cambian y qué no
Lee las evasiones públicas de un paquete de Puppeteer y Playwright, las tres superficies que un parche de página no alcanza y un fixture de valor, descriptor y toString.
BotBrowser Team
Quieres la documentación estructurada de Huella digital?
Este artículo forma parte de la biblioteca editorial. Para pasos de configuración, material de referencia y actualizaciones continuas, entra en la sección de docs.
Un plugin de este tipo es un conjunto de parches de JavaScript de página, y la forma de esa entrega decide tanto lo que puede cambiar como lo que no. El paquete puppeteer-extra-plugin-stealth distribuye un directorio de evasiones, cada una un pequeño script evaluateOnNewDocument que reescribe una propiedad de navigator, un miembro del espacio de nombres chrome o un getter de WebGL antes de que se ejecute el código de la propia página. Es un mecanismo real e inspeccionable, y tiene un límite: un parche que se ejecuta dentro de la página se puede leer desde dentro de la página, y tres superficies no pasan nunca por el JavaScript de página. Leer el código fuente público de ese paquete basta para enumerar lo que corrige, nombrar las tres superficies que no puede alcanzar y construir un fixture que lee valor, descriptor y toString en paralelo, de modo que se pueda distinguir si un parche se aplicó de verdad. Las variantes de plugin para Puppeteer y para Playwright son proyectos de terceros separados con sus propios ciclos de publicación, y ninguna de las dos cuenta con recomendación. Una tasa de aprobación no es algo que un cambio local pueda prometer.
BotBrowser proporciona otra ruta de entrega para los mismos valores de documento: los ajustes de perfil se aplican dentro del núcleo del navegador, así que un valor y la ruta de código que lo produce vienen de una misma capa, como documenta la referencia de coherencia de automatización. Esa capa sigue a un solo lado de la frontera descrita abajo, por lo que no garantiza un veredicto de terceros, y las superficies de red y de proceso quedan fuera de su alcance.
TL;DR
- Las evasiones del plugin son scripts de página inyectados antes del código del propio documento, no configuración del navegador. Cada una reescribe un valor visible desde JavaScript, y una página puede leer el resultado de vuelta.
- El directorio público de evasiones contiene 19 entradas:
chrome.app,chrome.csi,chrome.loadTimes,chrome.runtime,defaultArgs,iframe.contentWindow,media.codecs,navigator.hardwareConcurrency,navigator.languages,navigator.permissions,navigator.plugins,navigator.vendor,navigator.webdriver,sourceurl,user-agent-override,webgl.vendor,window.outerdimensions, más las carpetas compartidas_templatey_utils. navigator.webdriverse trata dos veces: la capa de argumentos añade--disable-blink-features=AutomationControlled, y el script de página elimina la propiedad del prototipo en las compilaciones antiguas de Chromium donde todavía está presente.- Un valor parcheado y uno sin parchear son indistinguibles con
console.log. LeerObject.getOwnPropertyDescriptoryFunction.prototype.toStringes lo que separa una propiedad reescrita de una nativa. - Tres superficies no llegan nunca al JavaScript de página: las huellas de la capa de red, el saludo TLS y los ajustes de HTTP/2; el proceso del navegador y su línea de comandos; y cualquier cosa que un servidor decida desde la conexión y no desde el documento.
Contenido
- Qué contiene realmente la lista pública de evasiones
- Qué no puede alcanzar un parche de JavaScript de página
- Un fixture que lee valor, descriptor y toString
- Conclusión práctica
Qué contiene realmente la lista pública de evasiones
El código del plugin es un directorio llamado evasions. Leerlo basta para saber qué toca el plugin, porque cada subdirectorio es una evasión con nombre propio y su propio index.js.
| Evasión | Superficie objetivo | Qué cambia el script |
|---|---|---|
navigator.webdriver | Marca de automatización | Elimina la propiedad del prototipo en compilaciones antiguas; añade un argumento de lanzamiento en las nuevas |
navigator.vendor | Identidad del navegador | Reescribe la cadena de proveedor |
navigator.plugins | Enumeración de plugins | Reemplaza el contenido del arreglo de plugins |
navigator.languages | Lista de idiomas | Reescribe el arreglo de idiomas informado |
navigator.permissions | Consulta de permisos | Envuelve el resultado de la consulta |
navigator.hardwareConcurrency | Número de núcleos | Reescribe el entero informado |
webgl.vendor | Parámetro de WebGL | Envuelve getParameter para las cadenas de proveedor y de renderizador |
chrome.app, chrome.csi, chrome.loadTimes, chrome.runtime | Espacio de nombres chrome | Añade o reescribe miembros que la página puede leer |
media.codecs | Compatibilidad de códecs | Reescribe la respuesta sobre códecs |
iframe.contentWindow | Identidad entre marcos | Alinea los valores que informa la ventana del iframe |
window.outerdimensions | Geometría de la ventana | Informa el tamaño exterior que tendría una ventana normal |
user-agent-override | Cabecera y valor del agente | Define la cabecera y la cadena visible para la página |
sourceurl | URL de origen del script | Retira el marcador sourceURL que Puppeteer añade a sus scripts inyectados |
defaultArgs | Argumentos de lanzamiento | Descarta valores por defecto de Puppeteer que hacen al navegador más fácil de identificar |
Dos hechos estructurales pesan más que las filas individuales. Primero, cada fila es una reescritura de un valor que una página puede leer, lo que significa que la reescritura es también un valor que una página puede inspeccionar. Segundo, la lista es finita y pública, así que quien escriba un detector puede leer el mismo directorio y sondear exactamente esas superficies.
El mecanismo de entrega es la restricción determinante
evaluateOnNewDocument ejecuta una función antes de cualquier script de página. Ese momento es lo que hace invisible el parche a una lectura ingenua: cuando el código de la página se ejecuta, la propiedad ya lleva el valor reescrito. Pero el parche no cambia el navegador; cambia lo que el documento ve después de que el navegador haya producido un valor.
Esa distinción explica toda la clase de fallo. Un servidor que decide a partir del saludo TLS no ejecuta nunca el script de página, así que ningún evaluateOnNewDocument lo alcanza. Un cliente CDP que inspecciona la línea de comandos del objetivo lee los argumentos del proceso, no el documento, de modo que una reescritura a nivel de página tampoco sirve ahí.
Las dos familias de plugins no comparten código
puppeteer-extra y playwright-extra son paquetes separados del mismo repositorio, cada uno con su propia interfaz de plugin. Un plugin escrito para uno no se carga en el otro sin una capa de adaptación, y los dos ecosistemas actualizan sus puntos de inyección con calendarios distintos. Eso importa para una comparación: una lista de evasiones leída del paquete de Puppeteer describe lo que ese paquete corrige en la ruta de lanzamiento de Puppeteer, y la variante de Playwright es un artefacto distinto con su propia lista.
La consecuencia práctica es que una afirmación sobre un plugin necesita una familia y una versión antes de significar algo. Dos equipos pueden decir que su navegador ejecuta un plugin con ese nombre y estar ejecutando código distinto con cobertura distinta, y la diferencia no se ve desde fuera.
Qué hace realmente la evasión de webdriver
La evasión navigator.webdriver tiene dos mitades, y leer las dos resulta instructivo. El gancho beforeLaunch modifica los argumentos de lanzamiento: si --disable-blink-features= ya está presente, añade AutomationControlled; en caso contrario añade la marca completa. El gancho onPageCreated ejecuta después un script de página que comprueba el valor actual y elimina Object.getPrototypeOf(navigator).webdriver solo en las compilaciones donde la propiedad sigue presente y verdadera.
El plugin es, por tanto, honesto sobre su propio límite: en una compilación de Chromium donde la marca ya produce false o undefined, el script de página no hace absolutamente nada. Es un detalle de diseño útil para quien lea una afirmación sobre un plugin, porque muestra que incluso sus autores tratan el argumento de lanzamiento como la corrección principal y el script de página como un parche de compatibilidad.
Qué no puede alcanzar un parche de JavaScript de página
Tres grupos de superficies están por debajo del documento o junto a él, y ningún script evaluateOnNewDocument puede cambiarlos.
La capa de red y TLS
El ClientHello de TLS, el orden de las suites de cifrado, las extensiones, la lista de protocolos ALPN, la trama SETTINGS de HTTP/2 y el orden de las cabeceras los produce la pila de red del navegador antes de que exista documento alguno. Un servidor que toma la huella de la conexión ve una forma a la que el script de página nunca tuvo acceso. Añadir o quitar una propiedad en navigator no altera un solo byte de ese saludo.
El mismo razonamiento cubre el comportamiento de DNS y la reutilización de conexiones. Un script de parche a nivel de página no puede cambiar qué resolutor respondió, si se reutilizó una conexión o cuánto duró el saludo TCP, porque esos eventos terminan antes de la primera línea de código de página.
El proceso del navegador y la línea de comandos
El árbol de procesos, la ruta del ejecutable, los argumentos en /proc/<pid>/cmdline en Linux y la relación padre-hijo entre un lanzador y su navegador son observables desde fuera del documento. Un script de página puede reescribir lo que el documento informa sobre sí mismo, pero no puede reescribir el vector de argumentos que inició el proceso.
Por eso también el plugin modifica args en beforeLaunch en lugar de intentar ocultar las marcas solo a la página. Las marcas son visibles para cualquiera que pueda leer el proceso, y el propio comentario del plugin sobre la barra de información de Chrome reconoce que el argumento tiene una consecuencia visible.
La decisión del lado del servidor
El último grupo no es una huella en absoluto; es el modelo que consume las lecturas. Un servicio de desafío puede combinar la forma de la red, las cabeceras de la petición, los tiempos, el historial de la cuenta y los valores que informa la página en una puntuación, y ninguna de esas entradas pasa por el script de página como un valor que pueda editar. Incluso un parche perfecto dentro del documento deja la puntuación al proveedor. Qué lecturas muestrea cada producto está inventariado en qué lee realmente un detector, y dos productos no tienen por qué muestrear el mismo conjunto.
Ese es el límite más profundo, y es el que convierte la expresión «navegador no detectado» en una afirmación sobre un modelo y no en una propiedad de un navegador.
Un fixture que lee valor, descriptor y toString
Un fixture que solo lee navigator.webdriver no puede distinguir un valor parcheado de uno nativo, porque ambos devuelven un booleano. Leer tres cosas en paralelo los separa.
Las tres mediciones
La primera es el valor en sí: navigator.webdriver, o navigator.vendor, o el resultado de WebGLRenderingContext.getParameter. Es lo que lee un detector ingenuo.
La segunda es el descriptor de la propiedad: Object.getOwnPropertyDescriptor(Object.getPrototypeOf(navigator), 'webdriver'). Un accesor nativo tiene una función get cuyo texto fuente nombra el getter integrado. Una propiedad eliminada devuelve undefined; una redefinida devuelve un descriptor cuyo get apunta a un envoltorio, que puede llevar o no los mismos indicadores que el accesor nativo.
La tercera es Function.prototype.toString sobre ese getter. El texto fuente de un accesor nativo lleva su propia firma y termina en [native code]; en una compilación de Chromium, el getter de Navigator.prototype.webdriver se lee como function get webdriver() { [native code] }. Un envoltorio definido en JavaScript muestra su propio cuerpo. La carpeta _utils del propio plugin contiene un ayudante withUtils que parchea toString exactamente por esta razón, lo cual es en sí mismo una prueba de que la lectura funciona. Lo que un toString blanqueado todavía deja ver se detalla aparte en funciones nativas sustituidas y qué ve una lista de toString, que enumera las comprobaciones que sobreviven a un texto fuente reescrito.
| Lectura | Respuesta nativa | Respuesta parcheada | Qué no puede probar |
|---|---|---|---|
| Valor leído | Un booleano, una cadena de proveedor, una cadena de renderizador | El valor reescrito | Cuál de los dos lo produjo |
getOwnPropertyDescriptor | Un accesor nativo con get y set | undefined, o un accesor redefinido | Si la página es coherente en lo demás |
Function.prototype.toString | [native code] | El propio código fuente del envoltorio | Si el envoltorio resulta convincente |
Por qué el fixture pertenece a una página propia
La lectura tiene que ejecutarse sobre un documento que controle el operador, porque el objetivo es comparar una configuración conocida con un documento conocido, y no sondear el sitio de otra persona. Basta una URL data: local o un archivo estático en disco: el script del fixture lee los tres valores, los imprime como JSON y guarda el resultado junto al registro de la ejecución.
La comparación es entonces trivial. Si el valor difiere de lo que informaría de forma nativa la compilación del navegador, el descriptor dice si la propiedad se eliminó o se reemplazó, y toString dice si el envoltorio se anuncia. Un parche que deja una cadena [native code] donde vive un envoltorio es un parche que también parcheó toString, que es exactamente el caso para el que existe el ayudante _utils del plugin.
Por qué el fixture necesita una línea base limpia
La comparación solo funciona si el fixture tiene algo con lo que comparar. Lee los mismos tres valores en la misma compilación con el plugin desactivado y guarda esa línea base junto a la ejecución parcheada. Sin ella, una cadena [native code] no prueba nada, porque no hay registro de cómo era el accesor sin parchear en esa compilación concreta.
Merece la pena conservar una segunda línea base: la misma lectura tomada de un lanzamiento simple y sin parchear de un Chromium estándar de la misma versión mayor. Esa pareja muestra qué diferencias vienen del plugin y cuáles de la propia compilación, porque un salto de versión de Chromium puede cambiar los indicadores del descriptor de un accesor sin que intervenga ningún plugin.
Leer un parche que afirma ser nativo
El caso interesante es un envoltorio cuyo toString también se ha parcheado para devolver [native code]. Eso no vuelve inútil la lectura; cambia lo que la lectura demuestra. Un descriptor cuyo get no es el getter del propio motor sigue difiriendo de la referencia sin parche tomada en la misma compilación, y un envoltorio que afirma [native code] mientras el texto fuente leído en otro realm difiere de la referencia sin parche es inconsistente consigo mismo. La comparación de identidad entre realms no sirve para esto: el getter de un iframe recién creado es un objeto función distinto incluso en un navegador sin ningún parche, así que esa comparación informa una diferencia en ambos casos.
Por eso el fixture debe registrar también los indicadores del descriptor: configurable, enumerable, writable y si set está presente. Un navigator.webdriver nativo es un accesor no escribible con un conjunto de indicadores estable, pero los indicadores merecen registrarse sin ser el criterio: una evasión que borra la propiedad no deja descriptor alguno, mientras que el ayudante replaceProperty del plugin extiende el descriptor existente y solo cambia el get, así que un getter sustituido conserva los indicadores nativos. Ese registro no exige que la página sea honesta sobre toString.
Registrar lo que vio el fixture
Guarda las tres lecturas, más el identificador de compilación del navegador, la versión del script inyectado y la URL del documento, en un único objeto JSON por ejecución. Dos ejecuciones con la misma configuración deberían producir las mismas lecturas; una diferencia entre ellas significa que la inyección no se aplicó en una de las ejecuciones, lo que es un defecto de configuración y no un resultado del detector.
Qué no puede zanjar el fixture
El fixture describe el documento. No describe la conexión, el proceso ni el modelo. Una lectura perfecta de un navigator.webdriver parcheado en una página propia no dice nada sobre cómo puntuará un proveedor la sesión, y esa brecha es la razón por la que un registro por ejecución tiene que nombrar qué superficie midió.
Conserva ese límite en el propio registro. Un campo etiquetado surface: document es honesto; la misma lectura etiquetada verdict: pass no lo es, porque el fixture nunca ejecutó el modelo del proveedor.
FAQ
¿Un parche a nivel de página cambia la huella de red del navegador?
No. Sus evasiones son scripts de página inyectados antes del código del documento, más un cambio en los argumentos de lanzamiento. Los bytes del ClientHello de TLS, la trama SETTINGS de HTTP/2 y el orden de las cabeceras los produce la pila de red antes de que exista documento alguno, así que un script a nivel de página no puede alcanzarlos.
¿Basta un navigator.webdriver parcheado para pasar un detector?
No. Un detector puede leer el valor, el descriptor de la propiedad, toString, los argumentos de lanzamiento o la forma de la red, y el modelo del proveedor decide qué lee. La evasión toca una sola propiedad del prototipo por dos rutas de código, mientras los indicadores del descriptor, el texto fuente leído en otro realm y la forma de la conexión siguen siendo lo que produjo el lanzamiento.
¿Cómo se distingue una propiedad parcheada de una nativa?
Lee tres cosas en una página propia: el valor, Object.getOwnPropertyDescriptor sobre el prototipo y Function.prototype.toString sobre el get del descriptor. Registra los indicadores del descriptor para la ejecución, y después lee el texto fuente dentro del realm del iframe con el Function.prototype.toString de ese realm y compáralo con la referencia sin parche tomada en la misma compilación. Un envoltorio con toString parcheado sigue difiriendo de la referencia una vez comparado el texto fuente registrado.
Conclusión práctica
Parte de la lista de evasiones y no de la etiqueta. El directorio público nombra cada superficie que toca el plugin, y todas ellas son valores que una página puede leer de vuelta. Acepta después las tres superficies que están por debajo del documento: la capa de red y TLS, el proceso del navegador y la línea de comandos, y el modelo del lado del servidor que consume todas las lecturas.
La siguiente acción es una ejecución del fixture en una página propia. Lee el valor, el descriptor y toString de navigator.webdriver, imprímelos como JSON junto a la compilación del navegador y la versión del script inyectado, y repite la ejecución una vez. Si las dos coinciden, el parche se aplicó de forma consistente; si difieren, lo que hay que investigar es la inyección y no el detector. Material adyacente: qué lee realmente un detector cubre la lista de superficies desde el lado del detector, y el registro de detectores BotD muestra qué comprobaciones ejecuta un detector de código abierto.
Sources
Este artículo forma parte de la guía: ¿Qué es la huella digital del navegador?
Artículos Relacionados
Funciones nativas sustituidas y qué ve una lista de toString
Huella digitalNavegador no detectado: qué lee un detector
Huella digitalBotD: qué detectores se ejecutan de verdad
DespliegueSeñales de automatización y coherencia en Playwright
Huella digitalQué prueban realmente las suites de detección públicas
Huella digitalPor qué una señal rara no condena a un navegador
Ejecuta la misma comprobación en BotBrowser
Carga un perfil, mantén dentro del navegador las señales que acabas de leer y confirma el resultado en tu propia máquina antes del despliegue.