Se hai mai provato a fare scraping con requests o httpx e ricevuto un 403 istantaneo senza nemmeno toccare un CAPTCHA, molto probabilmente il colpevole è la tua impronta TLS. L'impersonazione TLS con curl_cffi è oggi una delle tecniche più efficaci per far sembrare il tuo client HTTP un browser reale a livello di handshake, ma da sola non basta: senza proxy residenziali di qualità, anche un ClientHello perfetto viene bloccato dal reputation scoring.
In questa guida tecnica vediamo come i sistemi anti-bot leggono il tuo stack TLS, come curl_cffi replica esattamente il fingerprint di Chrome, perché i proxy residenziali restano indispensabili e come implementare tutto questo con ProxyHat.
Cos'è l'impersonazione TLS con curl_cffi e perché esiste
L'impersonazione TLS con curl_cffi è la capacità di una libreria HTTP Python di presentare un ClientHello identico a quello di un browser reale durante l'handshake TLS. Il problema nasce da un fatto fondamentale: il client HTTP di Python non è un browser, e i server anti-bot lo sanno benissimo.
Quando apri una connessione TLS verso un sito protetto da Cloudflare, Akamai, DataDome o PerimeterX, il server non aspetta di vedere il tuo User-Agent per decidere se sei umano. Molto prima che una singola intestazione HTTP venga inviata, il server analizza il ClientHello: il primo pacchetto che il tuo client invia per negoziare la connessione cifrata. Questo pacchetto contiene una lista di cipher suite, estensioni, curve ellittiche supportate e altri parametri che variano da client a client in modo prevedibile.
La libreria curl_cffi è un binding Python per curl-impersonate, un fork di curl compilato con BoringSSL — la stessa libreria TLS usata da Chrome — invece del consueto OpenSSL. Questo permette di replicare non solo le cipher suite ma anche l'ordine esatto delle estensioni, i valori GREASE e la forma del frame HTTP/2 SETTINGS, producendo un JA3/JA4 indistinguibile da quello di un browser reale.
Perché il ClientHello di Python requests/urllib3 viene identificato
Il fingerprinting TLS si basa sull'osservazione che ogni client HTTP ha un modo caratteristico di costruire il ClientHello. Il progetto JA3 di Salesforce ha formalizzato questo concetto: si concatenano i parametri del ClientHello (versione TLS, cipher suite, estensioni, curve ellittiche, formati di firma) in un hash MD5 che identifica univocamente il client.
Ecco cosa rende immediatamente riconoscibile un client Python basato su OpenSSL/urllib3:
1. Ordine delle cipher suite
OpenSSL ordina le cipher suite in modo diverso rispetto a BoringSSL. Chrome priorizza TLS_AES_128_GCM_SHA256 come prima cipher di TLS 1.3, mentre OpenSSL può presentarla in posizione diversa o includere cipher legacy che Chrome non usa. Un server anti-bot confronta l'ordine esatto: se le prime 10 cipher non corrispondono al pattern Chrome conosciuto, il punteggio di rischio aumenta.
2. Estensioni e loro ordinamento
Chrome invia circa 17-20 estensioni TLS in un ordine specifico: server_name (SNI), extended_master_secret, renegotiation_info, supported_groups, ec_point_formats, session_ticket, application_layer_protocol_negotiation (ALPN con h2 e http/1.1), status_request, signature_algorithms, key_share, e altre. OpenSSL ha un ordine diverso e spesso omette estensioni come encrypted_client_hello (ECH) o delegated_credentials che Chrome moderno include.
3. Valori GREASE
Chrome inserisce valori GREASE (Generate Random Extensions And Satisfy Emulation) — cipher e estensioni fittizie come 0x0a0a o 0x1a1a — per prevenire problemi di compatibilità futuri. Questi valori appaiono in posizioni pseudo-casuali ma deterministiche per versione. OpenSSL non li include quasi mai, quindi la loro assenza è un segnale immediato: non sei un browser.
4. Curve ellittiche supportate
Chrome annuncia x25519, secp256r1, secp384r1 in questo ordine preciso. OpenSSL può includere x25519 ma l'ordine e la presenza di curve aggiuntive differiscono. Il JA3 hash include questi valori, e un mismatch è sufficiente per un ban.
5. Forma del ClientHello TLS 1.3
Il ClientHello di TLS 1.3 ha una struttura più complessa con key_share che include già i parametri DH per x25519 e secp256r1. Chrome include entrambi i key share nel primo messaggio, mentre molti client Python ne includono solo uno o nessuno, costringendo il server a un round-trip aggiuntivo. Questo pattern è rilevabile.
Un JA3 di Python requests tipico è771,4865-4866-4867-49195-49199-...con un hash come5d41388d.... Il JA3 di Chrome 120 è completamente diverso, con GREASE e un set di estensioni più ampio. La differenza è immediatamente visibile al server.
Come curl_cffi replica il fingerprint di Chrome
La libreria curl_cffi affronta il problema alla radice: invece di provare a riconfigurare OpenSSL per somigliare a BoringSSL, usa direttamente BoringSSL. Questo significa che il ClientHello generato è bit-per-bit identico a quello di Chrome, non una approssimazione.
Il ruolo di BoringSSL
BoringSSL è il fork di OpenSSL mantenuto da Google, usato internamente da Chrome e Android. Le sue cipher predefinite, l'ordine delle estensioni e i valori GREASE sono hardcoded per corrispondere al comportamento del browser. Quando curl_cffi compila curl-impersonate con BoringSSL, eredita automaticamente queste caratteristiche.
Impostazione di impersonate="chrome"
L'API di curl_cffi espone un parametro impersonate che seleziona un preset precompilato:
from curl_cffi import requests
# Versione sincrona
r = requests.get(
"https://example.com",
impersonate="chrome"
)
# Versione asincrona
from curl_cffi.requests import AsyncSession
async def fetch():
async with AsyncSession(impersonate="chrome") as s:
r = await s.get("https://example.com")
print(r.status_code)
Il preset "chrome" mappa alla versione più recente di Chrome supportata dalla libreria. Puoi anche specificare versioni esplicite come "chrome110", "chrome116", "chrome120", "chrome124" per bloccare un fingerprint specifico. Questo è utile quando un sito aggiorna i suoi controlli e una versione più recente introduce variazioni che non vuoi ancora adottare.
Override del fingerprint: ja3, akamai, extra_fp
Per casi avanzati, curl_cffi permette di sovrascrivere singoli componenti del fingerprint:
from curl_cffi.requests import AsyncSession
async def fetch_custom():
async with AsyncSession() as s:
r = await s.get(
"https://example.com",
impersonate="chrome120",
ja3="771,4865-4866-4867-49195-49199,...",
akamai="1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,p",
extra_fp={
"tls_signature_algorithms": "0401,0501,0601,0201",
"tls_client_hello": "16030100c8010000c40303...",
}
)
- ja3: sovrascrive completamente la stringa JA3 (versione, cipher, estensioni, curve, formati firma).
- akamai: sovrascrive il fingerprint HTTP/2 (SETTINGS frame, window size, priority frames) che Akamai usa per identificare i client.
- extra_fp: permette di personalizzare singoli campi come algoritmi di firma o persino un ClientHello raw in esadecimale.
Il fingerprint HTTP/2 è spesso trascurato ma altrettanto importante: Chrome invia un frame SETTINGS con parametri specifici (HEADER_TABLE_SIZE=65536, INITIAL_WINDOW_SIZE=6291456, MAX_HEADER_LIST_SIZE=262144), e l'ordine dei frame di priorità è caratteristico. Akamai Bot Manager controlla proprio questi valori.
Chrome 110+ e la permutazione del ClientHello: perché JA4 esiste
A partire da Chrome 110, Google ha introdotto una permutazione pseudo-casuale dell'ordine di alcune estensioni del ClientHello. Questo significa che due istanze di Chrome 110+ che si connettono allo stesso server possono presentare estensioni in ordine leggermente diverso, pur essendo entrambe legittime.
Questa scelta è stata fatta per rendere il fingerprinting TLS più difficile: se l'ordine delle estensioni cambia ad ogni connessione, un hash che dipende dall'ordine (come JA3) diventa inaffidabile. Ma questo crea anche un problema per chi fa impersonazione: se il tuo client presenta sempre lo stesso ordine, è rilevabile come non-browser.
Il design di JA4
JA4 è stato progettato da FoxIO come risposta a questo problema. A differenza di JA3 che concatena i parametri nell'ordine in cui appaiono, JA4 ordina i parametri alfabeticamente prima di fare l'hash. Questo lo rende order-stable: due client con lo stesso set di cipher ed estensioni producono lo stesso JA4 anche se l'ordine di trasmissione differisce.
Il formato JA4 è più strutturato: JA4 = t_i_c_s_hash dove t è il tipo (TCP o QUIC), i indica se SNI è presente, c è il numero di cipher, s è il numero di estensioni, e hash è l'hash SHA256 dei parametri ordinati.
Per l'impersonazione, questo significa che curl_cffi non deve replicare l'ordine esatto per passare i controlli basati su JA4 — deve solo presentare lo stesso set di parametri. Ma molti sistemi anti-bot usano ancora JA3 o una combinazione di JA3+JA4, quindi curl_cffi replica comunque l'ordine esatto di Chrome per massima compatibilità.
Perché i proxy residenziali restano obbligatori
Un errore comune è pensare che un TLS fingerprint perfetto sia sufficiente. Non lo è. Anche se il tuo ClientHello è identico a quello di Chrome 120, se l'IP di provenienza è 45.12.34.56 registrato come OVH, Hetzner o AWS, il server anti-bot applica un punteggio di reputazione che quasi sempre supera la soglia di blocco.
I sistemi anti-bot moderni usano un approccio multilayer:
| Livello di controllo | Cosa rileva | Peso nel punteggio |
|---|---|---|
| IP reputation | ASN datacenter, IP blacklist, geolocalizzazione | 40-50% |
| TLS fingerprint (JA3/JA4) | Cipher, estensioni, GREASE, curve | 20-30% |
| HTTP/2 fingerprint | SETTINGS frame, priority, window size | 10-15% |
| Browser fingerprint JS | Canvas, WebGL, font, User-Agent consistency | 15-20% |
| Comportamentale | Pattern di navigazione, timing, mouse | 5-10% |
Un IP datacenter con un TLS fingerprint perfetto ottiene un punteggio di circa 40-50 su 100 solo per la reputazione IP, che spesso basta per un blocco. Un IP residenziale con lo stesso fingerprint parte da 5-15, lasciando ampio margine. Questo è il motivo per cui curl_cffi deve essere combinato con proxy residenziali di qualità.
ProxyHat offre proxy residenziali con IP assegnati da ISP reali in oltre 190 paesi, con rotazione automatica e sessioni sticky configurabili. Puoi consultare la lista completa delle locazioni disponibili o vedere i piani e prezzi.
Esempio pratico: curl_cffi + ProxyHat residential
Vediamo un esempio completo e funzionante: una sessione asincrona curl_cffi con impersonazione Chrome, instradata attraverso i proxy residenziali di ProxyHat con targeting geografico tedesco.
Setup base con curl_cffi e ProxyHat
import asyncio
from curl_cffi.requests import AsyncSession
PROXY = "http://user-country-DE:YOUR_PASSWORD@gate.proxyhat.com:8080"
async def fetch_with_impersonation(url: str):
async with AsyncSession(impersonate="chrome120") as session:
response = await session.get(
url,
proxy=PROXY,
timeout=30
)
return {
"status": response.status_code,
"ja3_passed": response.status_code != 403,
"body_length": len(response.content),
"headers": dict(response.headers)
}
async def main():
result = await fetch_with_impersonation(
"https://www.cloudflare.com/cdn-cgi/trace"
)
print(result)
asyncio.run(main())
Questo esempio base usa impersonate="chrome120" per presentare il fingerprint di Chrome 120 e instrada il traffico attraverso un exit node residenziale in Germania. Il parametro user-country-DE nel username specifica il targeting geografico.
Rotazione e retry con sessioni multiple
Per scraping serio, serve rotazione IP e retry automatici. Ecco un pattern più robusto che combina curl_cffi con rotazione ProxyHat:
import asyncio
import random
from curl_cffi.requests import AsyncSession
GATEWAY = "gate.proxyhat.com"
PORT = 8080
USERNAME = "YOUR_USERNAME"
PASSWORD = "YOUR_PASSWORD"
COUNTRIES = ["DE", "US", "GB", "FR", "NL"]
MAX_RETRIES = 3
async def fetch_with_rotation(url: str, session_id: str = None):
for attempt in range(MAX_RETRIES):
country = random.choice(COUNTRIES)
# Costruisci il proxy URL con rotazione geografica
user_part = f"user-country-{country}"
if session_id:
user_part += f"-session-{session_id}"
proxy_url = f"http://{user_part}:{PASSWORD}@{GATEWAY}:{PORT}"
try:
async with AsyncSession(impersonate="chrome120") as session:
response = await session.get(
url,
proxy=proxy_url,
timeout=30,
allow_redirects=True
)
if response.status_code == 200:
return response
elif response.status_code == 403:
print(f"Attempt {attempt+1}: 403 from {country}, rotating...")
continue
else:
print(f"Attempt {attempt+1}: status {response.status_code}")
if response.status_code < 500:
return response
except Exception as e:
print(f"Attempt {attempt+1} failed: {e}")
await asyncio.sleep(1 + attempt * 2)
raise Exception(f"All {MAX_RETRIES} attempts failed")
async def scrape_multiple(urls: list[str]):
tasks = []
for i, url in enumerate(urls):
session_id = f"sess-{i}-{random.randint(1000,9999)}"
tasks.append(fetch_with_rotation(url, session_id))
results = await asyncio.gather(*tasks, return_exceptions=True)
return results
async def main():
urls = [
"https://httpbin.org/headers",
"https://www.cloudflare.com/cdn-cgi/trace",
"https://example.com",
]
results = await scrape_multiple(urls)
for url, result in zip(urls, results):
if isinstance(result, Exception):
print(f"FAILED: {url} -> {result}")
else:
print(f"OK: {url} -> {result.status_code}")
asyncio.run(main())
Test rapido con curl
Per verificare rapidamente che il fingerprint TLS passi i controlli, puoi usare curl direttamente da terminale:
# Test con proxy residenziale ProxyHat (Germania)
curl --proxy http://user-country-DE:YOUR_PASSWORD@gate.proxyhat.com:8080 \
--impersonate chrome120 \
https://tls.peet.ws/api/all
# Verifica il JA3/JA4 restituito
# Un fingerprint Chrome corretto dovrebbe mostrare:
# ja3_hash: 1121bc8994c6e0a9b7f53e9b1b... (varia per versione)
# ja4: t13d1516h2_8daaf6152771_b186095e22b6
Il sito tls.peet.ws restituisce il tuo JA3 e JA4 effettivi, permettendoti di verificare che l'impersonazione funzioni correttamente prima di puntare su siti reali.
Errori comuni e casi limite
1. User-Agent incoerente
Se usi impersonate="chrome120" ma lasci un User-Agent predefinito di curl_cffi o di una versione Chrome diversa, il server rileva l'incoerenza tra TLS fingerprint e HTTP headers. Sempre assicurati che l'User-Agent corrisponda alla versione Chrome che stai impersonando:
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/120.0.0.0 Safari/537.36",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.9",
"Accept-Encoding": "gzip, deflate, br",
}
async with AsyncSession(impersonate="chrome120") as session:
response = await session.get(url, proxy=PROXY, headers=headers)
2. Mismatch HTTP/2 vs HTTP/1.1
Chrome usa HTTP/2 (ALPN h2) per quasi tutte le connessioni. Se il tuo server supporta HTTP/2 ma il tuo client negozia HTTP/1.1, è un segnale. curl_cffi con BoringSSL negozia h2 automaticamente, ma se forzi http_version a 1.1, perdi coerenza.
3. Dimenticare il fingerprint HTTP/2 (Akamai)
Molti si concentrano solo sul JA3 e dimenticano che Akamai Bot Manager controlla anche il frame SETTINGS di HTTP/2. Se il tuo JA3 è perfetto ma il fingerprint HTTP/2 è quello di curl predefinito, Akamai ti blocca. Usa il parametro akamai o lascia che il preset impersonate lo gestisca automaticamente.
4. Usare SOCKS5 senza configurazione corretta
Se preferisci SOCKS5, ProxyHat supporta la porta 1080. Assicurati di usare il formato corretto:
SOCKS5_PROXY = "socks5://user-country-DE:YOUR_PASSWORD@gate.proxyhat.com:1080"
async with AsyncSession(impersonate="chrome120") as session:
response = await session.get(url, proxy=SOCKS5_PROXY)
5. Sessioni sticky per flussi multi-richiesta
Se un sito richiede login o cookie di sessione, devi usare sessioni sticky per mantenere lo stesso IP tra richieste. Con ProxyHat, basta aggiungere -session-NOMESESSIONE al username:
proxy = "http://user-country-DE-session-mysess123:PASSWORD@gate.proxyhat.com:8080"
Per approfondire i pattern di scraping con proxy, consulta la nostra guida sui casi d'uso del web scraping e sul SERP tracking.
Limiti e considerazioni etiche
curl_cffi non risolve le sfide JavaScript
Questo è il limite più importante da capire. L'impersonazione TLS fa passare il tuo client attraverso il primo livello di controllo (fingerprint TLS + IP reputation), ma non esegue JavaScript. Se un sito usa challenge JS complessi — come Cloudflare Turnstile, DataDome JS challenge, o Akamai Bot Manager con sensor data — curl_cffi non può risolverli.
In questi casi hai due opzioni:
- Usare un browser headless con stealth (Playwright + stealth plugin, o undetected-chromedriver) per risolvere la challenge e poi estrarre i cookie/token di sessione per passarli a curl_cffi per le richieste successive.
- Usare un servizio di risoluzione CAPTCHA se la challenge è un CAPTCHA esplicito.
Accesso autorizzato e conformità legale
L'impersonazione TLS è una tecnica neutra: può essere usata per ricerca di sicurezza, test di penetrazione autorizzati, o accesso a dati pubblici legittimi. Ma è fondamentale rispettare:
- robots.txt: anche se non legalmente vincolante in tutte le giurisdizioni, rispettarlo è buona pratica e riduce il rischio di conflitti.
- Termini di servizio (ToS): molti siti proibiscono esplicitamente l'accesso automatizzato. Violare i ToS può avere conseguenze legali.
- CFAA (Computer Fraud and Abuse Act, USA): negli Stati Uniti, l'accesso non autorizzato a sistemi informatici può configurare violazione del CFAA, anche senza intento fraudolento. La giurisprudenza è evoluta, ma il rischio esiste.
- GDPR (UE): se raccogli dati personali (anche indirettamente, come IP o cookie), devi avere una base giuridica valida e rispettare i principi di minimizzazione dei dati.
Per orientarti nella configurazione dei proxy, consulta la documentazione ufficiale di ProxyHat.
Key Takeaways
- L'impersonazione TLS con curl_cffi replica il ClientHello di Chrome a livello bit usando BoringSSL, producendo JA3/JA4 identici a quelli di un browser reale.
- Python
requests/urllib3 con OpenSSL ha un JA3 immediatamente riconoscibile per ordine delle cipher, estensioni mancanti, assenza di GREASE e forma del ClientHello TLS 1.3.- Il preset
impersonate="chrome120"gestisce automaticamente cipher, estensioni, GREASE, curve ellittiche e fingerprint HTTP/2. Per casi avanzati, usaja3,akamaieextra_fp.- Chrome 110+ permuta l'ordine delle estensioni; JA4 è progettato per essere order-stable, ordinando i parametri prima dell'hash.
- I proxy residenziali sono obbligatori: un TLS fingerprint perfetto su un IP datacenter fallisce comunque il reputation scoring (40-50% del punteggio totale).
- curl_cffi non esegue JavaScript: per challenge JS complesse serve un browser reale o un servizio di risoluzione.
- Rispetta sempre robots.txt, ToS, CFAA e GDPR. L'accesso a dati pubblici legittimi è il caso d'uso appropriato.
FAQ
Cos'è l'impersonazione TLS con curl_cffi?
L'impersonazione TLS con curl_cffi è la tecnica di presentare un ClientHello TLS identico a quello di un browser reale (tipicamente Chrome) durante l'handshake. curl_cffi usa BoringSSL invece di OpenSSL per replicare cipher suite, estensioni, valori GREASE, curve ellittiche e fingerprint HTTP/2 di Chrome, producendo JA3/JA4 che i sistemi anti-bot non riescono a distinguere da quelli di un browser legittimo.
Perché l'impersonazione TLS con curl_cffi è importante per chi usa proxy?
I sistemi anti-bot moderni controllano il fingerprint TLS prima ancora di leggere gli header HTTP. Un client Python con OpenSSL ha un JA3 immediatamente riconoscibile come non-browser, che porta a blocchi istantanei indipendentemente dalla qualità del proxy. curl_cffi risolve questo problema a livello di protocollo, permettendo al traffico proxy di passare il primo filtro anti-bot. Senza impersonazione TLS, anche i migliori proxy residenziali possono essere inefficaci su siti protetti.
Quale tipo di proxy funziona meglio con l'impersonazione TLS curl_cffi?
I proxy residenziali sono la scelta ottimale perché combinano un TLS fingerprint credibile (gestito da curl_cffi) con un IP assegnato da ISP reali, che ha un punteggio di reputazione basso. I proxy datacenter hanno IP registrati a hosting provider (AWS, OVH, Hetzner) che i sistemi anti-bot classificano automaticamente come ad alto rischio. I proxy mobili offrono la reputazione più alta ma a costo maggiore. Per la maggior parte dei casi d'uso di scraping, i residenziali offrono il miglior rapporto qualità-prezzo.
Come evitare i blocchi quando si implementa l'impersonazione TLS con curl_cffi?
Per evitare i blocchi: usa proxy residenziali con rotazione geografica, mantieni coerenza tra la versione Chrome impersonata e l'User-Agent HTTP, usa sessioni sticky per flussi multi-richiesta che richiedono cookie, non dimenticare il fingerprint HTTP/2 (preset akamai), e limita la frequenza delle richieste per non triggerare controlli comportamentali. Se il sito usa challenge JavaScript, usa un browser headless per risolverle e poi passa i cookie a curl_cffi.
curl_cffi può sostituire completamente un browser per lo scraping?
No. curl_cffi replica il fingerprint TLS e HTTP/2 di Chrome ma non esegue JavaScript. Per siti che richiedono esecuzione JS (Cloudflare Turnstile, DataDome, challenge sensor-based di Akamai), serve un browser reale come Playwright con stealth plugin. La strategia ottimale è ibrida: usa curl_cffi per la maggior parte delle richieste (veloci, efficienti) e un browser headless solo per risolvere le challenge iniziali, poi riutilizza i cookie di sessione con curl_cffi.






