AntiDetect
Funzionalità Tecnologia Prezzi Per chi è
Docs Blog FAQ
Lingua
EnglishItalianoDeutschEspañolРусский
Scarica

Dieci modi in cui il DevTools Protocol ti tradisce

Ogni stack di automazione diffuso pilota Chrome attraverso il DevTools Protocol. CDP è stato costruito per il debug, non per nascondersi — e lascia tracce in una decina di punti. Ecco dove, e perché una patch in JavaScript di solito peggiora le cose.

CDP non è mai stato pensato per essere silenzioso

Puppeteer, Playwright, Selenium 4 e ogni wrapper "stealth" costruito su di essi condividono una sola base: il Chrome DevTools Protocol. È lo stesso canale che il tuo browser apre quando premi F12. Chrome lo espone perché un debugger possa ispezionare e pilotare la pagina, ed è stato progettato con esattamente un non-obiettivo: nascondere il fatto che un debugger sia collegato.

È da qui che nasce il problema. I segnali qui sotto non sono bug di Puppeteer. Quasi tutti sono CDP che si comporta correttamente, in modi in cui un browser guidato da un umano non si comporta mai. A un fornitore di sistemi di rilevamento non serve rompere nulla per trovarli; gli basta leggere valori che la piattaforma regala.

Quello che segue è l'elenco aggiornato al 2026, ordinato grosso modo per quanto è economico verificare ciascun punto.

navigator.webdriver, e perché correggerlo in JS è peggio

Il segnale più vecchio. navigator.webdriver restituisce true ogni volta che Chrome viene avviato con l'automazione attiva. Ogni tutorial ti dice di sovrascriverlo:

Object.defineProperty(navigator, 'webdriver', { get: () => false })

Questa non è una soluzione. È un secondo segnale, più rumoroso. In un browser di serie webdriver è un accessor su Navigator.prototype, non una proprietà propria dell'istanza. Dopo lo snippet qui sopra, tre cose sono vere che non sono vere da nessun'altra parte:

Gli script di rilevamento percorrono la catena toString da anni. Un browser che restituisce l'onesto true è un browser che usa l'automazione. Un browser che restituisce un false falsificato è un browser che usa l'automazione e cerca di nasconderlo, che per quasi tutti i motori di rischio è la cosa peggiore fra le due.

L'unica versione di questo che sopravvive a un'ispezione è quella in cui è il getter C++ stesso a restituire false, così non c'è nessuna proprietà propria, nessuna arrow function e niente fuori posto nella catena dei prototipi.

Runtime.enable: il leak che ha riscritto lo stack di tutti

Per eseguire JavaScript in una pagina, quasi tutte le librerie chiamano Runtime.enable. Quel singolo comando ha effetti collaterali che arrivano dentro la pagina: inizia a emettere notifiche Runtime.executionContextCreated e cambia il modo in cui vengono serializzati gli argomenti della console.

L'exploit classico è abbastanza corto da stare in un tweet. Crea un oggetto con un getter, passalo a console.debug e guarda se il getter scatta. Con un debugger collegato e Runtime.enable chiamato, il protocollo serializza l'oggetto per il client e il getter viene eseguito. Senza debugger, niente lo tocca.

let leaked = false;
const probe = { get id() { leaked = true; return 1; } };
console.debug(probe);
// leaked === true  →  something is listening on CDP

È questo che ha spinto l'ecosistema verso fork con patch — rebrowser-puppeteer e simili — che evitano Runtime.enable e usano invece mondi isolati o addBinding. Quelle patch funzionano e vale la pena usarle. Chiudono anche esattamente un buco su dieci.

Tre difetti nell'input sintetico

Quando l'automazione muove un mouse, non muove un mouse. Chiama Input.dispatchMouseEvent, e Chrome costruisce un oggetto evento che è quasi, ma non del tutto, quello che produce la vera pipeline di input. Tre differenze sono banalmente osservabili da una pagina.

screenX e screenY sono sbagliati

È il bug 40280325 di Chromium. CDP imposta screenX = clientX e screenY = clientY — coordinate del viewport là dove ci vogliono coordinate dello schermo. Per un clic vero i due differiscono per la posizione della finestra più la chrome del browser: barra delle schede, barra degli indirizzi, segnalibri. Su una finestra massimizzata sono circa 90-130 pixel di scostamento verticale.

Così un clic autentico vicino alla cima di una pagina ha screenY intorno a 150. Un clic CDP nello stesso punto riporta screenY = 20. Cloudflare Turnstile tratta un screenY basso come un clic da bot, e fa bene: nessun umano può cliccare sopra la chrome del proprio browser.

UIEvent.detail è zero

Per un clic vero, event.detail porta il conteggio dei clic — 1 per un clic singolo, 2 per un doppio. Gli eventi di clic inviati via CDP arrivano con click_count = 0, quindi detail è 0. Non esiste gesto dell'utente che produca un clic con detail a zero. Turnstile lo controlla dentro il suo iframe di sfida.

getCoalescedEvents() è vuoto

I browser moderni raggruppano i movimenti del puntatore ad alta frequenza e ne espongono i campioni grezzi tramite PointerEvent.getCoalescedEvents(). Un pointermove vero porta sempre almeno un evento raggruppato: sé stesso. Un pointermove inviato via CDP porta un array vuoto, perché non c'è mai stato un campione hardware da raggruppare.

Nessuna di queste cose si può riparare da JavaScript. L'oggetto evento viene costruito in C++ prima che qualsiasi script della pagina possa vederlo, e le proprietà sono di sola lettura.

Finestra del browser in sezione che mostra lo scostamento fra clientY e screenY
Bug 40280325 di Chromium. Un clic vero porta la chrome del browser nella sua coordinata di schermo; un clic CDP riporta invece la coordinata del viewport. Turnstile legge la differenza.

Le stringhe che l'automazione lascia in giro

Due famiglie di stringhe letterali finiscono dove una pagina può leggerle.

Marcatori di ChromeDriver. ChromeDriver tiene traccia dei risultati delle proprie chiamate definendo proprietà del documento con prefisso cdc_, $cdc_ o $chrome_. Non sono nascoste. Un ciclo di tre righe su Object.getOwnPropertyNames(document) le trova, e gli script di rilevamento cercano esattamente questo dal 2018.

URL sorgente di Puppeteer. Puppeteer aggiunge //# sourceURL=pptr:... agli script che valuta e crea un mondo isolato chiamato __puppeteer_utility_world__. Entrambi emergono in Error.stack se un'eccezione attraversa il confine:

try { null.f() } catch (e) { /* e.stack may contain "pptr:" */ }

Sono i segnali più facili da rimuovere e quelli più spesso lasciati al loro posto, perché compaiono solo in condizioni che uno sviluppatore prova di rado: un errore non catturato dentro una funzione valutata.

Worker: il contesto che nessuno copre

È la falla che frega le configurazioni per il resto più attente.

Web Worker, Service Worker e Shared Worker hanno ciascuno il proprio scope globale con il proprio navigator. Quell'oggetto viene creato dal motore, nel thread del worker, a partire dai valori reali del browser. Gli override CDP a livello di pagina — Emulation.setUserAgentOverride, script iniettati, Page.addScriptToEvaluateOnNewDocument — non lo raggiungono.

Il risultato è un browser che dichiara una cosa sul thread principale e un'altra dentro un worker. A uno script di rilevamento basta chiedere due volte:

// main thread says 8; worker says 24
new Worker(URL.createObjectURL(new Blob([
  'postMessage([navigator.hardwareConcurrency, navigator.deviceMemory, navigator.platform])'
], { type: 'text/javascript' })));

Qualsiasi discordanza fra i due è decisiva. I browser veri non possono produrne una, perché entrambi gli scope leggono gli stessi valori sottostanti. Fra i segnali che sfuggono per questa via ci sono hardwareConcurrency, deviceMemory, platform, l'intero elenco di brand di userAgentData e — tramite OffscreenCanvas — il vendor e il renderer WebGL.

Sistemare questo dall'esterno non è possibile in modo completo. I valori devono venire dal motore, il che significa che il navigator del worker va costruito a partire dallo stesso profilo di quello della finestra.

Le piccole cose che comunque si sommano

Altre tre, ciascuna economica da verificare e ciascuna capace di chiudere una sessione da sola.

Dimensioni esterne con i DevTools agganciati

La differenza fra outerWidth e innerWidth è la chrome del browser — circa 16 pixel in orizzontale e 85-90 in verticale su Chrome desktop. Aggancia i DevTools e quel divario salta di centinaia di pixel. Una finestra che riporta outerHeight - innerHeight = 400 è una finestra con un debugger aperto.

Quota di archiviazione

navigator.storage.estimate() restituisce una quota derivata dallo spazio libero su disco. È già di suo un forte segnale hardware, e rivela anche container e contesti in incognito, che riportano numeri tondi caratteristici molto al di sotto di qualsiasi disco reale.

API mancanti per la piattaforma che dichiari

Se il tuo user agent dice Chrome 147 su Windows, la pagina può controllare le API che Chrome 147 su Windows ha davvero. Le build headless storicamente non avevano chrome.runtime, l'intero flusso dei permessi di Notification e il supporto a codec presenti nelle build headful. Dichiarare una piattaforma che non puoi sostenere è peggio che non dichiarare nulla.

Perché la soluzione deve stare nei sorgenti

Guarda l'elenco e uno schema emerge. Quasi ogni segnale nasce sotto JavaScript:

Uno script di contenuto viene eseguito dopo tutto questo. Può solo sovrascrivere ciò che è già stato deciso, e ogni sovrascrittura lascia le tracce descritte sopra: proprietà proprie dove ci vogliono i prototipi, arrow function dove ci vuole codice nativo, disaccordo fra contesti.

È questo l'argomento onesto a favore di una build modificata di Chromium, e vale la pena enunciarne anche i limiti. Applicare patch al motore sistema lo strato sotto JavaScript. Non sistema i fingerprint TLS e HTTP/2, che stanno del tutto sotto il browser, e non fa nulla per il comportamento: una sessione che clicca con regolarità disumana verrà segnalata a prescindere da quanto siano pulite le sue proprietà.

Testare la propria configurazione

Non fidarti della parola di un fornitore, nemmeno della nostra. I test sono pubblici e li puoi eseguire in un pomeriggio:

Eseguili su quello che usi oggi prima di cambiare qualsiasi cosa. Uno strumento che passa nove di questi test e fallisce il decimo non è a nove decimi del percorso: una sola discordanza decisiva basta.

Risolto nel motore, non in uno script di contenuto

P8 distribuisce una build di Chromium in cui queste cose si comportano come si comporta un browser normale — perché è stato cambiato il C++, non ci è stato messo un involucro sopra.

Inizia gratis
Tutti gli articoli Prossimo: fingerprinting del browser spiegato