L'impersonazione TLS con curl_cffi è oggi una delle tecniche più efficaci per superare i controlli anti-bot basati sul fingerprint della connessione. Se hai mai provato a fare scraping con requests e ricevuto un 403 immediato senza nemmeno toccare il WAF, probabilmente il problema non era il tuo User-Agent: era il tuo ClientHello TLS. In questa guida tecnica vediamo perché i sistemi anti-bot riescono a distinguere Python da Chrome analizzando pochi byte crittografici, come curl_cffi risolve il problema replicando il fingerprint di un browser reale, e perché anche un TLS perfetto non basta senza proxy residenziali di qualità.
Cosa rende individuabile il ClientHello TLS di Python
Quando un client TLS avvia una connessione verso un server HTTPS, il primo pacchetto che invia è il ClientHello. Questo messaggio contiene la lista dei cipher suite supportati, le estensioni TLS, le curve ellittiche, i metodi di firma e — in TLS 1.3 — i parametri del key exchange. La combinazione e l'ordine di questi elementi costituiscono un fingerprint univoco della tua implementazione TLS.
Il problema è che le librerie Python come urllib3 e requests si appoggiano a OpenSSL o a LibreSSL, che hanno un ClientHello profondamente diverso da quello di Chrome o Firefox. Le differenze sono molteplic e individuabili:
- Ordine dei cipher suite: OpenSSL ordina i cipher secondo una logica di priorità diversa da BoringSSL (il fork di OpenSSL usato da Chrome). La lista di cipher è quasi un segnale distintivo quanto l'IP.
- Estensioni TLS: Chrome include estensioni specifiche come
encrypted_client_hello(ECH),application_settings, egreasein posizioni precise. OpenSSL tipicamente non include GREASE o lo posiziona diversamente. - Curve ellittiche e point formats: Chrome annuncia
x25519,secp256r1esecp384r1in un ordine specifico, mentre OpenSSL può elencarne altre o in ordine diverso. - GREASE (RFC 8701): Chrome inserisce valori GREASE casuali nelle liste di cipher, estensioni e curve per prevenire fossilizzazione del fingerprint. La loro assenza è un segnale immediato che non sei un browser.
- Shape del ClientHello TLS 1.3: La struttura del messaggio in TLS 1.3 differisce significativamente da TLS 1.2, e il modo in cui Chrome negozia la versione (estensione
supported_versions) è caratteristico.
Il risultato è che un server può calcolare l'hash JA3 del tuo ClientHello — una funzione di hash MD5 applicata alla concatenazione di version TLS, cipher suite, estensioni, curve ellittiche e point formats — e confrontarlo con un database di fingerprint noti. Il JA3 di requests con OpenSSL è noto e catalogato: qualsiasi WAF moderno lo riconosce in meno di 1 ms.
Per approfondire il formato JA3, la documentazione di riferimento è il repository originale JA3 di Salesforce, che descrive l'algoritmo di fingerprinting nel dettaglio.
JA3 vs JA4: perché il fingerprinting è diventato order-stable
Il JA3 originale ha un punto debole: l'ordine degli elementi è parte dell'hash. Questo significa che se un browser permuta l'ordine dei cipher suite — come Chrome fa dalla versione 110+ — il JA3 cambia completamente, rendendo il fingerprint instabile. I team di anti-bot hanno quindi fatto fatica a distinguere un Chrome reale che permuta da un client che cerca di evadere il fingerprinting.
Per risolvere questo, JA4 è stato progettato per essere order-stable: ordina alfabeticamente i cipher suite e le estensioni prima di calcolare l'hash, rendendo il fingerprint indipendente dall'ordine. Questo significa che:
- Chrome 110+ con permutazione dei cipher produce lo stesso JA4 di Chrome 109 senza permutazione.
- Un client non-browser che cerca di imitare Chrome deve comunque avere lo stesso set di cipher ed estensioni, non solo lo stesso ordine.
- JA4 rende più difficile evadere il fingerprinting riordinando casualmente i cipher, ma rende anche più affidabile il rilevamento di client legittimi.
JA4 include anche sotto-fingerprint come JA4S (ServerHello), JA4H (HTTP), e JA4L (latency), creando un profilo multidimensionale molto più difficile da spoofare. Per le specifiche tecniche di JA4, consulta il repository JA4 di FoxIO.
Come curl_cffi replica il fingerprint di Chrome
curl_cffi è un binding Python per curl-impersonate, una versione modificata di curl che usa BoringSSL invece di OpenSSL/GnuTLS. BoringSSL è la stessa libreria TLS usata da Chrome, quindi il ClientHello generato è strutturalmente identico a quello del browser.
Ma non basta cambiare libreria TLS: curl-impersonate modifica anche l'ordine dei cipher, le estensioni, le curve, e persino il frame HTTP/2 SETTINGS che viene inviato dopo il handshake TLS. Chrome, infatti, annuncia parametri HTTP/2 specifici (HEADER_TABLE_SIZE, INITIAL_WINDOW_SIZE, ecc.) che differiscono dai default di curl. Un WAF che ispeziona il traffico HTTP/2 può confrontare questi parametri con quelli attesi da Chrome.
Impostazione del fingerprint con impersonate="chrome"
curl_cffi espone un parametro impersonate che seleziona un preset preconfigurato:
from curl_cffi import requests
response = requests.get(
"https://httpbin.org/headers",
impersonate="chrome"
)
print(response.status_code)
Il preset "chrome" imposta automaticamente:
- Cipher suite nell'ordine esatto di Chrome.
- Estensioni TLS incluse GREASE nelle posizioni corrette.
- Curve ellittiche e point formats.
- User-Agent e header HTTP coerenti con la versione di Chrome impersonata.
- Parametri HTTP/2 SETTINGS identici a Chrome.
I preset disponibili includono "chrome", "safari", "edge", e versioni specifiche come "chrome110" o "chrome116". Questo è fondamentale: un fingerprint Chrome 110 inviato con un User-Agent che dichiara Chrome 120 genera un'incongruenza che i sistemi anti-bot più avanzati rilevano immediatamente.
Override granulari: ja3, akamai, extra_fp
Per casi d'uso avanzati, curl_cffi permette di sovrascrivere singoli componenti del fingerprint:
from curl_cffi import requests
response = requests.get(
"https://example.com",
impersonate="chrome",
ja3="771,4865-4866-4867-49195-49199,0-23-65279-10-11-35-16-5-13-18-51-45-43-27-17513,29-23-24,0",
akamai="2,1406516315;2,1406516315;3,1459236204;4,57600504;5,1459236204;6,262144;7,1459236204;8,1459236204",
extra_fp={
"tls_signature_algorithms": [
0x0401, 0x0501, 0x0601, 0x0201,
0x0403, 0x0503, 0x0603, 0x0203,
0x0202, 0x0401, 0x0501, 0x0601
]
}
)
Il parametro ja3 sovrascrive la stringa JA3 completa, akamai imposta i parametri HTTP/2 SETTINGS, e extra_fp permette override su signature algorithms e altri dettagli. Questo è utile quando un sito usa un WAF che richiede un fingerprint specifico non coperto dai preset standard.
Confronto: requests vs curl_cffi vs browser reale
| Caratteristica | requests (OpenSSL) | curl_cffi (BoringSSL) | Chrome reale |
|---|---|---|---|
| Libreria TLS | OpenSSL 3.x | BoringSSL | BoringSSL |
| JA3 match con Chrome | No | Sì (con preset) | Sì (nativo) |
| GREASE in ClientHello | Assente o errato | Presente, corretto | Presente |
| HTTP/2 SETTINGS | N/A (HTTP/1.1) | Replicato da preset | Nativo Chrome |
| JA4 stability | Stabile ma individuabile | d>Stabile, match ChromeStabile | |
| Risoluzione JS challenge | No | No | Sì |
| Overhead prestazionale | ~0ms | ~5-15ms | ~200-500ms |
Perché i proxy residenziali restano obbligatori
Un fingerprint TLS perfetto è necessario ma non sufficiente. Anche se il tuo ClientHello è identico a quello di Chrome 120, se la connessione arriva da un IP datacenter noto (AWS, DigitalOcean, OVH), il sistema anti-bot applica un reputation score all'IP che può abbassare il tuo trust score sotto la soglia di blocco indipendentemente dal TLS.
I sistemi anti-bot moderni come Cloudflare Bot Management, Datadome e PerimeterX combinano segnali multipli:
- IP reputation: IP datacenter hanno una probabilità storica di traffico bot molto più alta degli IP residenziali ASN.
- ASN classification: Un IP che appartiene a un ASN cloud provider (AS14618 per AWS, AS16509 per Amazon) viene penalizzato rispetto a un IP di un ISP residenziale (Comcast, Deutsche Telekom, Telecom Italia).
- Geolocation consistency: Se il tuo IP è in Germania ma il tuo header Accept-Language è
ja-JP, c'è un'incongruenza. - Behavioral analytics: Pattern di navigazione troppo regolari, tempo tra le richieste troppo uniforme, assenza di interazioni mouse/touch.
Un proxy residenziale fornisce un IP con ASN ISP residenziale, che ha un reputation score di base significativamente più alto. Secondo dati pubblici di Cloudflare Bot Management, il sistema assegna un bot score da 1 a 99 dove IP datacenter con fingerprint non-browser ricevono spesso score inferiori a 10, mentre IP residenziali con fingerprint browser coerenti possono raggiungere 70-90.
Per questo motivo, la combinazione curl_cffi + proxy residenziali è la strategia più efficace per scraping e automazione legittima. Puoi esplorare le locazioni proxy disponibili su ProxyHat per scegliere exit node in paesi specifici.
Esempio pratico: curl_cffi AsyncSession con ProxyHat
Vediamo un esempio completo e funzionante che combina curl_cffi con i proxy residenziali di ProxyHat. Usiamo un AsyncSession per gestire connessioni multiple con rotazione IP e retry.
Setup base con proxy HTTP
import asyncio
from curl_cffi.requests import AsyncSession
async def scrape_with_proxyhat():
async with AsyncSession(
impersonate="chrome120",
proxies={
"https": "http://user-country-DE:password@gate.proxyhat.com:8080",
"http": "http://user-country-DE:password@gate.proxyhat.com:8080"
},
timeout=30
) as session:
response = await session.get("https://httpbin.org/ip")
print(f"Status: {response.status_code}")
print(f"IP: {response.json()['origin']}")
print(f"JA3 match: {response.headers.get('x-ja3-hash', 'N/A')}")
asyncio.run(scrape_with_proxyhat())
Rotazione IP e retry con sessioni sticky
Per progetti di scraping su larga scala, è necessario ruotare gli IP e gestire i retry. ProxyHat supporta sessioni sticky tramite il flag session nel username, che mantiene lo stesso IP per tutta la durata della sessione:
import asyncio
import random
import string
from curl_cffi.requests import AsyncSession
async def scrape_with_rotation():
targets = [
"https://httpbin.org/ip",
"https://httpbin.org/headers",
"https://httpbin.org/user-agent"
]
async with AsyncSession(impersonate="chrome120", timeout=30) as session:
for target in targets:
# Genera un session ID casuale per ogni request
# → nuovo IP ad ogni richiesta
session_id = "".join(random.choices(string.ascii_lowercase + string.digits, k=12))
proxy_url = f"http://user-country-DE-session-{session_id}:password@gate.proxyhat.com:8080"
for attempt in range(3):
try:
response = await session.get(
target,
proxies={"https": proxy_url, "http": proxy_url}
)
if response.status_code == 200:
print(f"[{target}] OK - IP: {response.json().get('origin', '?')}")
break
elif response.status_code == 403:
print(f"[{target}] Blocked, retry {attempt + 1}/3")
await asyncio.sleep(2 ** attempt)
else:
print(f"[{target}] Status {response.status_code}")
break
except Exception as e:
print(f"[{target}] Error: {e}, retry {attempt + 1}/3")
await asyncio.sleep(2 ** attempt)
asyncio.run(scrape_with_rotation())
ProxyHat SDK per rotazione automatica
Per un'integrazione più robusta, puoi combinare curl_cffi con il pattern di rotazione di ProxyHat usando geo-targeting a livello di città:
import asyncio
from curl_cffi.requests import AsyncSession
# Lista di città tedesche per geo-rotazione
GERMAN_CITIES = ["berlin", "munich", "frankfurt", "hamburg", "cologne"]
async def scrape_with_geo_rotation():
async with AsyncSession(impersonate="chrome120", timeout=30) as session:
for city in GERMAN_CITIES:
proxy_url = f"http://user-country-DE-city-{city}:password@gate.proxyhat.com:8080"
try:
response = await session.get(
"https://httpbin.org/ip",
proxies={"https": proxy_url, "http": proxy_url}
)
print(f"[{city}] {response.json()['origin']}")
except Exception as e:
print(f"[{city}] Error: {e}")
asyncio.run(scrape_with_geo_rotation())
Per casi d'uso che richiedono SOCKS5 (ad esempio per tunneling attraverso reti più restrittive), ProxyHat supporta anche il protocollo SOCKS5 sulla porta 1080:
proxy_url = "socks5://user-country-DE:password@gate.proxyhat.com:1080"
Puoi approfondire le strategie di scraping nella nostra guida al web scraping con proxy residenziali e nella guida al SERP tracking. Per i dettagli di configurazione completi, consulta la documentazione ufficiale ProxyHat.
Errori comuni e casi limite
1. Mismatch tra versione Chrome impersonata e User-Agent
Se usi impersonate="chrome110" ma il tuo User-Agent dichiara Chrome/120.0.6099.71, un WAF avanzato rileva l'incongruenza. curl_cffi imposta automaticamente l'User-Agent coerente con il preset, ma se lo sovrascrivi manualmente assicurati che la versione corrisponda.
2. Dimenticare gli header HTTP secondari
Un ClientHello perfetto non basta se i tuoi header HTTP non sono coerenti con Chrome. Header come sec-ch-ua, sec-ch-ua-platform, sec-fetch-dest, sec-fetch-mode, e sec-fetch-site sono inviati da Chrome ma spesso omessi nei scraper. curl_cffi con preset chrome include alcuni di questi header, ma devi verificare che siano completi.
3. Non gestire il retry con backoff esponenziale
Anche con fingerprint TLS perfetto e proxy residenziali, alcune richieste falliranno per rate limiting temporaneo o transient errors. Implementa sempre un retry con backoff esponenziale (come nell'esempio sopra) e rispetta i Retry-After header quando presenti.
4. Usare curl_cffi per sfide JavaScript
curl_cffi non esegue JavaScript. Se il sito usa Cloudflare Turnstile, Datadome CAPTCHA, o challenge JS che richiedono esecuzione di codice lato client, curl_cffi non può risolverli. In questi casi, hai bisogno di un browser reale (Playwright, Puppeteer) con plugin stealth, o di servizi specializzati. curl_cffi è ottimale per siti dove il blocco è basato su TLS/IP reputation, non su JS challenge.
5. Concorrenza eccessiva da un singolo IP
Anche con sessioni sticky, inviare 100 richieste concorrenti dalla stessa uscita residenziale può triggerare rate limiting. Distribuisci le richieste su più sessioni/IP e mantieni una concorrenza ragionevole (10-20 richieste per IP). Consulta il piano ProxyHat per i limiti di concorrenza del tuo tier.
Limiti e considerazioni etiche
L'impersonazione TLS è una tecnica potente che deve essere usata in modo responsabile. Alcuni punti fondamentali:
- Accesso autorizzato a dati pubblici: L'impersonazione TLS è legittima per accedere a dati pubblicamente disponibili (SERP, prezzi e-commerce pubblici, dati meteorologici) rispettando i
robots.txte i Termini di Servizio. - Computer Fraud and Abuse Act (CFAA): Negli Stati Uniti, il CFAA proibisce l'accesso non autorizzato a sistemi informatici. Sebbene l'accesso a dati pubblici sia generalmente considerato legittimo, l'uso di tecniche di evasione anti-bot su siti che lo vietano esplicitamente nei ToS può costituire una violazione. Consulta la guida CFAA del Department of Justice per il quadro legale.
- GDPR e privacy: Per dati che coinvolgono个人信息 personali di residenti UE, il GDPR si applica. Assicurati di avere una base giuridica per il trattamento e di rispettare i diritti degli interessati.
- Rate limiting responsabile: Anche se tecnicamente puoi inviare 1000 richieste al secondo, non dovresti. Rispetta i limiti del server e non causare degrado del servizio.
Regola d'oro: Se non saresti a tuo agio a spiegare la tua attività di scraping al proprietario del sito, probabilmente non dovresti farla.
Key Takeaways
- Il ClientHello TLS è un fingerprint: I sistemi anti-bot identificano Python/requests dal JA3/JA4 in meno di 1 ms. L'assenza di GREASE, l'ordine dei cipher errato e le estensioni mancanti sono segnali immediati.
- curl_cffi risolve il problema TLS: Usando BoringSSL e replicando cipher, estensioni, curve e HTTP/2 SETTINGS di Chrome, curl_cffi produce un ClientHello indistinguibile da quello del browser.
- JA4 è order-stable: A differenza di JA3, JA4 ordina i cipher prima di calcolare l'hash, rendendo il fingerprint indipendente dalla permutazione. Questo rende più difficile evadere il fingerprinting ma anche più affidabile identificare client legittimi.
- I proxy residenziali sono obbligatori: Un TLS perfetto su un IP datacenter fallisce comunque il reputation scoring. Combina curl_cffi con proxy residenziali come ProxyHat per massimizzare il successo.
- curl_cffi non risolve JS challenge: Per Cloudflare Turnstile, Datadome e simili, serve un browser reale. Usa curl_cffi per siti dove il blocco è TLS/IP-based.
- Etica e legalità: Usa l'impersonazione TLS solo per accesso autorizzato a dati pubblici, rispetta robots.txt e ToS, e considera il quadro CFAA/GDPR.
FAQ
Qual è la differenza tra JA3 e JA4 fingerprinting?
JA3 calcola un hash MD5 della concatenazione di version, cipher, estensioni, curve e point formats nell'ordine in cui appaiono nel ClientHello. JA4 ordina alfabeticamente questi elementi prima di calcolare l'hash, rendendo il fingerprint indipendente dall'ordine. JA4 è stato introdotto per gestire la permutazione dei cipher introdotta in Chrome 110+ e produce fingerprint più stabili e confrontabili.
Posso usare curl_cffi senza proxy?
Tecnicamente sì, ma è sconsigliato per scraping serio. Senza proxy, le tue richieste arrivano dal tuo IP reale, che può essere bloccato dopo poche richieste. Per uso legittimo su un singolo sito con poche richieste, può andare bene. Per scraping su larga scala, i proxy residenziali sono praticamente obbligatori per distribuire le richieste su IP con buona reputazione.
curl_cffi è sufficiente contro Cloudflare?
Dipende. Se il sito usa Cloudflare con TLS fingerprinting e IP reputation (Bot Management in modalità passiva), curl_cffi con proxy residenziali può essere sufficiente. Se il sito usa Cloudflare Turnstile o challenge JavaScript attivi, curl_cffi non può risolverli perché non esegue JavaScript. In quel caso serve un browser reale con plugin stealth.
Quale versione di Chrome devo impersonare?
Usa la versione più recente disponibile in curl_cffi (es. chrome120 o superiore) che corrisponda a un User-Agent plausibile. Evita versioni troppo vecchie (sotto Chrome 110) perché i WAF possono flaggare browser obsoleti come sospetti. Assicurati che l'User-Agent, i sec-ch-ua header e il preset curl_cffi siano coerenti tra loro.
Quante richieste concorrenti posso inviare con ProxyHat?
La concorrenza massima dipende dal tuo piano ProxyHat. In generale, mantieni 10-20 richieste concorrenti per IP residenziale per evitare rate limiting. Per throughput maggiore, distribuisci le richieste su più sessioni/IP usando il flag session nel username. Consulta il piano ProxyHat per i limiti specifici del tuo tier.






