Cómo funciona realmente la detección de Puppeteer: 12 señales que delatan a los bots
Los frameworks de automatización heredan la huella de un navegador real, pero la alteran de docenas de formas observables. Una guía de campo de las 12 señales de JavaScript, red y comportamiento que delatan a Puppeteer y Playwright.
Los frameworks de automatización controlan navegadores reales, lo que significa que heredan la huella del navegador. Un Chrome manejado por Puppeteer tiene el mismo user agent, la misma versión de Chrome y la misma build de Chromium que el Chrome de un humano. Superficialmente, parecen idénticos.
Pero los frameworks de automatización alteran el navegador de docenas de formas sutiles, la mayoría observables desde JavaScript. La detección no es magia. Es una lista de comprobación.
1. La bandera navigator.webdriver
La señal más simple y más famosa. Cuando Chrome se ejecuta bajo un framework de automatización, navigator.webdriver devuelve true. Forma parte de la especificación W3C WebDriver y Chromium la implementa por defecto.
Todo proyecto de automatización serio parchea esto en segundos. Puppeteer-extra-stealth sobrescribe el getter. Los usuarios de Playwright inyectan scripts con Object.defineProperty en cada documento nuevo. La bandera es trivial de ocultar.
Pero el hecho de que pueda ocultarse trivialmente es en sí mismo una señal. Un Chrome legítimo no necesita ocultar nada. Cuando una página comprueba Object.getOwnPropertyDescriptor(Navigator.prototype, 'webdriver') y el descriptor no coincide con el de un navegador nativo, la automatización queda atrapada incluso después de parchear la bandera.
2. La anomalía del objeto chrome
El Chrome legítimo expone un objeto global window.chrome con una estructura sustancial: chrome.runtime, chrome.loadTimes, chrome.csi. Chrome headless y las configuraciones antiguas de Puppeteer o bien omiten este objeto por completo o bien exponen una versión recortada.
Los plugins de sigilo recrean el objeto, pero la recreación es imperfecta. chrome.runtime.onConnect puede estar presente sin chrome.runtime.PlatformOs. Las firmas de las funciones pueden devolver objetos en lugar de undefined. Cada discrepancia es una señal positiva.
3. Inconsistencias de la API de permisos
Los navegadores reales devuelven resultados coherentes cuando se les consulta a través de la API de permisos. navigator.permissions.query({name: 'notifications'}) debería devolver 'default' si el usuario no ha concedido ni denegado el permiso explícitamente.
Chrome headless devuelve 'denied' por defecto, porque no hay interfaz donde mostrar un aviso de permiso. Los frameworks de automatización lo parchean, pero a menudo de forma incorrecta. Un delator habitual: consultar un permiso inusual como 'clipboard-read' devuelve un resultado que no coincide con lo que hace el Chrome real en la versión actual.
4. Los arrays de plugins y tipos MIME
navigator.plugins en un navegador real devuelve un PluginArray con entradas como el visor de PDF y el plugin de PDF de Chromium. En modo headless, este array está vacío.
Los plugins de sigilo añaden entradas falsas, pero esas entradas suelen tener propiedades erróneas: falta length, los campos de descripción son incorrectos, u objetos de plugin que no se comportan como instancias de Plugin cuando se prueban con instanceof.
5. Discrepancias de idioma y configuración regional
navigator.language y navigator.languages deberían coincidir con la cabecera HTTP Accept-Language. También deberían ser coherentes con la zona horaria reportada por Intl.DateTimeFormat().resolvedOptions().timeZone.
Un bot que afirma estar en Kiev (zona horaria Europe/Kyiv) pero envía Accept-Language: en-US,en;q=0.9 y reporta navigator.language === 'en-US' es posible, pero estadísticamente inusual. Combinado con una IP de un centro de datos de Fráncfort, el cuadro queda claro.
6. Las cadenas del renderizador de WebGL
WebGL2RenderingContext.getParameter(WebGLDebugRendererInfo.UNMASKED_RENDERER_WEBGL) devuelve el fabricante y el modelo de la GPU. En una máquina real, esto podría ser:
ANGLE (Intel, Intel(R) UHD Graphics 620 Direct3D11 vs_5_0 ps_5_0)
Chrome headless ejecutándose en un contenedor suele devolver:
ANGLE (Google, Vulkan 1.3.0 (SwiftShader Device (Subzero)), SwiftShader driver)
Cualquier cadena de GPU que contenga SwiftShader o Subzero es una señal fuerte de bot.
7. Anomalías de pantalla y viewport
Una ventana de navegador real tiene window.outerWidth y window.outerHeight que incluyen el marco del navegador: barra de direcciones, pestañas, barra de marcadores. La diferencia entre las dimensiones exteriores e interiores es típicamente de 80-140 píxeles en vertical.
Los navegadores headless a menudo tienen outerHeight === innerHeight porque no hay marco que mostrar. Puppeteer con headless: false lo corrige, pero muchas configuraciones de automatización siguen ejecutándose con dimensiones iguales.
8. Enumeración de fuentes
Los usuarios reales tienen cientos de fuentes instaladas, que varían según el SO, el idioma y las aplicaciones instaladas. Un Windows 11 con Office instalado tiene más de 500 fuentes. Los contenedores de automatización con Alpine Linux tienen 30.
La enumeración de fuentes mediante document.fonts.check() o el renderizado en un canvas fuera de pantalla lo revela de inmediato. Un navegador que se presenta como Chrome en Windows 11 pero solo expone 30 fuentes es casi con total seguridad automatización.
9. Entropía del movimiento del ratón
Los usuarios reales mueven el ratón con temblor. Las curvas no son lineales. La velocidad varía. Entre dos clics suele haber cientos de eventos mousemove.
Los frameworks de automatización sintetizan el movimiento, ya sea con líneas rectas hacia las coordenadas o con curvas programadas que carecen de los microtemblores de una mano humana. Incluso las simulaciones con curvas de Bézier tienden a tener perfiles de aceleración sospechosamente suaves.
Los sistemas de detección de bots recopilan las trazas de movimiento y las puntúan contra un modelo de movimiento humano. Las trazas con velocidad demasiado constante, curvas demasiado limpias o microeventos ausentes se marcan.
10. Firmas de temporización
Los frameworks de automatización ejecutan JavaScript con características de temporización distintas de las de los navegadores manejados por usuarios. La temporización de performance.now() entre eventos, especialmente entre pointerdown y pointerup, sigue una distribución diferente.
Los tiempos reales de pulsación de un clic son de 50-150ms con alta varianza. La duración de pulsación por defecto de Puppeteer es un valor fijo —a menudo 30ms o 100ms— casi sin varianza. Cien clics con tiempos de pulsación idénticos son automatización definitiva.
11. Discrepancia de la huella de TLS
Esta señal se salta JavaScript por completo. Cuando Puppeteer se conecta, usa la pila TLS de Chromium, la misma que un Chrome real. Pero los scrapers basados en undici o axios que fingen ser Chrome falseando el user agent usan la pila TLS de Node.js, que tiene una firma de Client Hello claramente distinta (hash JA4).
Una petición que afirma User-Agent: Chrome/124.0.6367.60 pero presenta una huella de TLS de Node.js es la confirmación instantánea de un cliente que no es un navegador.
12. Fugas del protocolo CDP
Puppeteer y Playwright se comunican con Chrome a través del Chrome DevTools Protocol. En algunas configuraciones, este protocolo deja artefactos detectables: propiedades extra en el objeto Runtime, formatos alterados de Error.stack o salidas específicas de console.debug que solo aparecen cuando hay un cliente CDP conectado.
El comando Runtime.enable, en particular, cambia cómo se renderizan los stack traces. Los bots que nunca provocan errores evitan esta señal, pero cualquier flujo de automatización que se tope con una excepción (y la mayoría lo hace) deja huellas.
Combinar estas señales en capas
Ninguna señal aislada es prueba de automatización. Un usuario real con un driver de GPU inusual puede devolver una cadena de WebGL extraña. Un usuario preocupado por la privacidad puede tener navigator.plugins modificado.
La fuerza de la detección moderna viene de combinar señales: 12 positivos débiles son más fuertes que un positivo fuerte, porque un usuario real rara vez dispara más de 2-3 anomalías a la vez.
Los sistemas de detección asignan un peso a cada señal y calculan una puntuación. Por encima del umbral, el visitante se trata como automatización. El umbral exacto y los pesos son la parte más difícil de acertar: demasiado estrictos, y se bloquea a usuarios legítimos; demasiado laxos, y los bots sofisticados se cuelan.
Las señales en sí son la parte fácil. Casi todos los frameworks de automatización filtran las 12. La verdadera ingeniería está en decidir qué combinaciones importan, cuáles son falsos positivos y cómo actualizar el modelo a medida que los frameworks de automatización se adaptan.