← Todos los artículos

Fingerprinting TLS: Por Que Bloquean tu Scraper de Python

Copias las cabeceras directamente de la pestana de red de tu navegador. User-Agent, Accept, Accept-Language, todo. Las pegas en un script de Python, envias la peticion y aun asi recibes un 403. Abres la misma URL en un navegador real y carga al instante.

Si te ha pasado, el bloqueo casi con seguridad no tiene que ver con tus cabeceras. Ocurre antes de que se lea una sola cabecera. El sitio esta leyendo tu handshake TLS, y tu cliente HTTP no se parece en nada a un navegador en esa capa.

Este articulo explica que es un fingerprint TLS, por que los clientes HTTP estandar destacan, como inspeccionar el tuyo y como camuflarte con herramientas de codigo abierto.

Que Es un Fingerprint TLS

Cada conexion HTTPS empieza con un handshake TLS. Lo primero que envia tu cliente es un mensaje ClientHello. Mucho antes de intercambiar cualquier URL, cookie o cabecera, ese mensaje ya revela mucho:

  • La version de TLS que soportas
  • La lista de cipher suites que ofreces, en un orden concreto
  • Las extensiones que anuncias (SNI, ALPN, grupos soportados, algoritmos de firma y mas)
  • Las curvas elipticas y formatos de punto que aceptas

Nada de esto es secreto, y todo es determinista por cliente. Una libreria TLS ensambla el ClientHello de la misma forma cada vez. Asi que puedes aplicar un hash a esos campos y obtener una cadena corta estable que identifica “que tipo de cliente es este”. Ese identificador es un fingerprint JA3 (y su sucesor mas robusto, JA4).

Lo importante: el fingerprint es una propiedad de la libreria TLS, no de tu codigo. requests de Python se apoya en urllib3, que usa el OpenSSL del sistema. Chrome usa BoringSSL. Firefox usa NSS. Cada uno construye el ClientHello de forma distinta: distinto orden de cifrados, distinto conjunto de extensiones, distinto relleno. Asi que un servidor que registra fingerprints JA3 puede distinguir “esto es una herramienta basada en OpenSSL” de “esto es Chrome” sin descifrar nada.

Por Que requests Destaca

Aqui esta el problema en una frase: los navegadores reales representan la abrumadora mayoria de los handshakes TLS de la web publica, y el stack por defecto de Python produce un handshake que ningun navegador produce.

Algunas razones concretas por las que tu cliente parece inusual:

  • Orden de las cipher suites. La lista de preferencias por defecto de OpenSSL no es la que trae Chrome, y el orden es parte del fingerprint.
  • Extensiones. Los navegadores envian extensiones como application_settings, valores GREASE y supported_versions especificos que un cliente OpenSSL puro no envia.
  • GREASE. Chrome inyecta a proposito valores “GREASE” aleatorios en sus listas de cifrados y extensiones para que los servidores toleren valores desconocidos. Su presencia (y patron) es en si misma una senal.

En conjunto, el JA3 de una instalacion por defecto de requests coincide con un conjunto pequeno y muy conocido de valores compartidos por scripts y bots. Los proveedores anti-bot mantienen listas de esos valores. Cuando tu handshake coincide con uno, pueden bloquearte, limitarte o servirte una pagina falsa, todo antes de que se lean tus cabeceras cuidadosamente copiadas.

Por eso tambien cambiar el User-Agent por uno de navegador no sirve de nada por si solo. Cambiaste una cabecera. El fingerprint que te delato vive una capa mas abajo.

Observa tu Propio Fingerprint

Puedes inspeccionar exactamente lo que envia tu cliente. Varios servicios publicos te devuelven tus detalles TLS. Compara un cliente basico con un navegador:

import requests

r = requests.get("https://tls.peet.ws/api/all")
data = r.json()
print(data["tls"]["ja3"])
print(data["tls"]["ja3_hash"])

Ejecuta eso y luego abre https://tls.peet.ws/api/all en Chrome y mira el campo ja3_hash. No coincidiran. El hash del navegador pertenece a un valor compartido por millones de usuarios reales; el hash de requests pertenece a un valor compartido sobre todo por otros scripts.

Esa unica comparacion suele ser el momento de “ahora lo entiendo”. Tus cabeceras estaban bien desde el principio.

Como Camuflarte: curl_cffi

La solucion mas limpia en Python es curl_cffi, un binding a curl-impersonate. Trae perfiles TLS que reproducen el ClientHello de navegadores reales byte a byte, incluidos el orden de cifrados, las extensiones y GREASE.

from curl_cffi import requests

# Imita un Chrome reciente - incluye fingerprint TLS + HTTP/2
r = requests.get(
    "https://example.com/api/products",
    impersonate="chrome",
    timeout=30,
)
print(r.status_code)
print(r.json())

La API es intencionadamente parecida a requests, asi que migrar codigo existente es sobre todo cuestion del import y del argumento impersonate. Ademas gestiona HTTP/2, lo cual importa (mas sobre esto abajo). Puedes fijar un objetivo concreto como impersonate="chrome124" o impersonate="firefox" cuando un sitio es exigente con una version en particular.

Verifica que funciono volviendo a llamar al servicio eco a traves de curl_cffi: el hash JA3 deberia coincidir ahora con un navegador real.

Otras Opciones

curl_cffi no es la unica via. Elige segun tu stack y lo estricto que sea el objetivo.

  • tls-client - un wrapper de Python sobre una implementacion TLS en Go con una amplia libreria de perfiles de navegador. Buena alternativa si un perfil de curl_cffi no coincide con un sitio concreto.
  • Un navegador real - Playwright o Selenium controlan un motor de navegador de verdad, asi que el fingerprint TLS es autentico por definicion. El coste es velocidad y memoria: un navegador es mucho mas pesado que un cliente HTTP, asi que reservalo para paginas que de verdad necesitan renderizado de JavaScript.
  • Node.js - el ecosistema es mas escaso, pero existen wrappers sobre curl-impersonate. Undici permite ajustar algunas opciones TLS, aunque igualar a un navegador con exactitud es mas dificil que en Python.

Un patron practico es calentar una sesion en un navegador real una vez, capturar las cookies y luego repetir peticiones baratas con un cliente HTTP que iguale el fingerprint. Obtienes confianza de navegador sin pagar el coste de navegador en cada llamada.

El Fingerprint Es Solo la Mitad de la Historia

Igualar tu JA3 te hace pasar la capa TLS, pero los sistemas anti-bot modernos comprueban mas de una cosa. Si arreglas el handshake y aun asi te bloquean, mira:

  • Orden y mayusculas de las cabeceras. HTTP/2 envia las cabeceras en un orden concreto y en minusculas. Si tu cliente envia cabeceras de navegador en el orden equivocado, eso es su propio fingerprint (a veces hasheado como “fingerprint HTTP/2” o fingerprint Akamai). Es una gran razon por la que importa el soporte HTTP/2: curl_cffi lo gestiona, requests a secas no habla HTTP/2 en absoluto.
  • Senales TCP/IP. Algunos sistemas identifican el sistema operativo por los tamanos de ventana y las opciones TCP. No puedes falsear esto facilmente desde espacio de usuario, y ahi es donde entran los proxies con IPs limpias de grado residencial.
  • Comportamiento. El ritmo de las peticiones, los patrones de navegacion y si cargas los sub-recursos de la pagina alimentan la puntuacion de reputacion.

La idea es pensar en capas. Una peticion bloqueada rara vez es un solo problema; es la suma de cada capa donde tu cliente difiere del navegador de un humano.

Cuando Dejar de Afinar

Igualar los fingerprints TLS y HTTP/2 tu mismo es muy factible para un punado de objetivos. Se vuelve tedioso a escala, porque los perfiles derivan: los navegadores se actualizan, los proveedores anti-bot ajustan, y un perfil que funcionaba el mes pasado empieza a fallar. Mantener una flota de fingerprints, proxies y logica de reintentos se convierte en su propio proyecto.

Si la carrera armamentistica del fingerprinting no es lo que quieres gestionar, una API de scraping se encarga de todo el stack por ti: un fingerprint TLS y HTTP/2 fiel al navegador, rotacion de IP y reintentos, tras una unica llamada HTTP. ScrapeUnblocker hace exactamente eso: envias una URL, devuelve la pagina real, y el handshake parece de un navegador real porque la peticion la hace infraestructura de navegador real. Es una alternativa limpia para los sitios donde los fingerprints afinados a mano dejan de merecer el mantenimiento. Puedes leer la documentacion de la API para ver como encaja en un scraper existente.

Preguntas Frecuentes

Puedo cambiar mi fingerprint TLS poniendo un User-Agent de navegador? No. El User-Agent es una cabecera HTTP enviada despues del handshake. Tu JA3 lo decide la libreria TLS que construye el ClientHello. Cambiar la cabecera no toca el fingerprint que te delato.

Es JA3 lo mismo que JA4? Sirven al mismo proposito - identificar un cliente por su ClientHello - pero JA4 es mas nuevo y mas resistente a la evasion trivial. Muchos sistemas ahora registran ambos. Herramientas como curl_cffi imitan navegadores reales lo bastante bien como para satisfacer los dos.

Usar un proxy arregla el fingerprinting TLS? No. Un proxy cambia tu direccion IP, no tu handshake. Puedes tener una IP residencial perfecta y aun asi ser bloqueado si tu JA3 grita “script de Python”. Normalmente necesitas ambas cosas: una IP limpia y un fingerprint parecido al de un navegador.

Un fingerprint que imita a un navegador me hara indetectable? Ningun arreglo unico lo hace. Elimina una senal fuerte. El orden de las cabeceras, los frames HTTP/2, la reputacion de la IP y el comportamiento siguen contando. Trata el fingerprint como necesario, no como suficiente.

Para Terminar

Cuando una peticion funciona en tu navegador pero no en tu script, resiste la tentacion de seguir ajustando cabeceras. Revisa la capa de debajo. Compara tu JA3 con un navegador real usando un servicio eco publico, y si difieren - lo haran - recurre a curl_cffi o a un motor de navegador real para cerrar la brecha.

Arregla el handshake primero. Luego preocupate por las cabeceras, HTTP/2, las IPs y el comportamiento, en ese orden. Y cuando mantener todo eso deje de ser un buen uso de tu tiempo, una API de scraping como ScrapeUnblocker existe para absorber el problema entero y que puedas volver a usar los datos.

Prueba ScrapeUnblocker gratis

Tasa de éxito del 95%+ · desde 0,55 € por cada 1000 llamadas · 500 solicitudes gratis al registrarte.

Pruébalo gratis → Ver precios