La difesa che si ritorce contro
Il fingerprinting del canvas funziona perché la rasterizzazione via GPU non è standardizzata a livello di pixel. Chiedi a due macchine di disegnare lo stesso testo con lo stesso font e la stessa dimensione e l'anti-aliasing differirà di un'inezia. Fai l'hash dei pixel e ottieni un valore stabile su un dispositivo e diverso sul successivo.
La contromisura intuitiva è aggiungere rumore: perturbare qualche pixel prima che venga calcolato l'hash, così il valore cambia e non può essere usato per tracciarti. Quasi ogni browser antidetect offre questa opzione, di solito etichettata "canvas: noise".
Il problema è che l'hash del canvas di un browser vero non cambia. Stessa macchina, stesso browser, stessa pagina, stesso risultato — oggi, domani e dopo un riavvio. Quella stabilità è l'intera ragione per cui vale la pena raccogliere questo segnale.
Quindi un valore che cambia a ogni lettura non è diventato anonimo. È diventato anomalo. È lo stesso tipo di errore che si fa impostando navigator.webdriver = false: la risposta onesta ti identifica, e quella disonesta ti identifica come qualcuno che mente.
Come funziona davvero il rilevamento
Rilevare la randomizzazione non richiede niente di ingegnoso. Quattro metodi, grosso modo in ordine di quanto spesso si incontrano:
Disegnare due volte la stessa cosa
Renderizzare due volte un canvas identico nello stesso caricamento di pagina e calcolare entrambi gli hash. Browser vero: identici. Rumore a ogni chiamata: diversi. Sono due righe di codice e stanno in ogni script di rilevamento serio.
Mediare via il rumore
Se il rumore è casuale e a media zero, allora renderizzare n volte e fare la media spinge il risultato verso il valore vero con una velocità di circa √n. Venti render bastano di solito a recuperare l'hash sottostante. La randomizzazione è costata al tracker qualche millisecondo e a te comunque l'anonimato — segnalandoti per giunta come uno che randomizza.
Controllare se il rumore ha senso
Un canvas randomizzato non è di per sé sospetto. Brave lo attiva per impostazione predefinita — il suo "farbling" deriva il rumore da un seed per sessione e per origine. Tor Browser chiede invece conferma. Quel che conta è se il rumore corrisponde al browser che dichiari di essere. Chrome non randomizza il canvas. Un client il cui user agent dice Chrome 147 e il cui canvas cambia a ogni chiamata si è contraddetto, e la contraddizione è il reperto, non il rumore.
Variare il percorso di rendering
Le implementazioni ingenue agganciano toDataURL() e getImageData(). Ci sono altre vie d'uscita: toBlob(), createImageBitmap(), il readPixels() di WebGL, OffscreenCanvas dentro un worker. Se un percorso ha il rumore e un altro no, i due non concordano — e quel disaccordo è una prova più forte di ciascuno dei due valori preso da solo.
Il percorso dei worker in particolare
Quest'ultimo merita una nota a parte, perché è il punto in cui quasi tutte le implementazioni si rompono.
OffscreenCanvas può essere trasferito in un Web Worker e renderizzato lì. Il worker ha il suo scope globale e la sua copia delle API del canvas, e l'iniezione di uno script a livello di pagina non lo raggiunge. Se il tuo rumore viene applicato da JavaScript iniettato, il canvas del worker esce pulito.
const off = new OffscreenCanvas(280, 60);
const w = new Worker(url);
w.postMessage({ canvas: off }, [off]);
// the worker draws and hashes — untouched by any page-level hook
Ora la pagina ha due hash del canvas provenienti dallo stesso dispositivo che non coincidono. Non esiste configurazione legittima in cui questo accada. È lo stesso problema strutturale descritto nell'articolo su CDP: qualsiasi cosa applicata dall'interno della pagina non può raggiungere un contesto che la pagina non possiede.
Com'è fatta un'implementazione corretta
L'obiettivo non è "un valore diverso ogni volta". È un valore diverso dagli altri, e lo stesso valore ogni volta per me. Detta così, i requisiti vengono da soli:
| Proprietà | Requisito |
|---|---|
| Nello stesso caricamento di pagina | Identico a ogni render |
| Fra sessioni diverse | Identico per lo stesso profilo, per sempre |
| Fra profili diversi | Diverso, e non per uno scostamento costante |
| Fra percorsi di rendering diversi | 2D, WebGL, OffscreenCanvas e worker concordano tutti |
| Plausibilità | Coerente con la GPU che il profilo dichiara |
Il meccanismo che soddisfa tutti e cinque i requisiti è un seed. Fissa un seed casuale alla creazione del profilo, salvalo e ricava da lì la perturbazione dei pixel in modo deterministico. Ogni render di quel profilo produce lo stesso output perché il seed è lo stesso. Due profili differiscono perché differiscono i loro seed. E siccome la derivazione avviene dove i pixel vengono prodotti — nel rasterizzatore, non in un metodo JavaScript agganciato — ogni percorso riceve lo stesso trattamento, worker compresi.
Quest'ultimo punto è il motivo per cui è difficile farlo per bene senza un motore modificato. Non c'è modo supportato di raggiungere il rasterizzatore di un worker da uno script di contenuto.
Le ragioni per non toccarlo
Esiste un argomento serio, che merita di essere esposto con onestà, per non toccare affatto il canvas.
Se usi un profilo per macchina fisica, il tuo hash del canvas autentico è un valore del tutto ordinario appartenente a un computer del tutto ordinario. È stabile, coerente su ogni percorso e corrisponde alla tua GPU perché è la tua GPU. Niente in esso invita a guardare due volte.
Il momento in cui questo smette di funzionare è quello in cui avvii un secondo profilo sulla stessa macchina. Ora i due profili producono lo stesso hash, e qualsiasi piattaforma che faccia fingerprinting del canvas può collegarli — che è esattamente l'esito che volevi evitare.
Quindi la decisione non è "rumore o niente rumore". È:
- Un'identità per macchina — lascia stare il canvas. Hardware vero, hash vero, niente da spiegare.
- Più identità per macchina — ti servono valori di canvas per profilo, e devono essere stabili e coerenti. La randomizzazione a ogni chiamata è la peggiore delle tre opzioni, perché non ti nasconde e non impedisce il collegamento.
Provare da soli
Non serve un fornitore di sistemi di rilevamento per verificarlo. Apri la console del browser su una pagina qualsiasi ed esegui:
function h() {
const c = document.createElement('canvas');
const x = c.getContext('2d');
x.textBaseline = 'top';
x.font = '14px Arial';
x.fillText('P8 canvas probe \u{1F511}', 2, 2);
return c.toDataURL().slice(-32);
}
console.log(h(), h(), h());
Tre stringhe identiche sono quello che ti dà un browser vero. Tre stringhe diverse significano rumore a ogni chiamata, e significano che qualsiasi sito che esegua le stesse tre righe lo sa.
Poi rieseguilo dopo aver riavviato il profilo. Il valore dovrebbe essere lo stesso di prima, se vuoi che il profilo sembri un dispositivo e non un bersaglio mobile — e diverso da quello che producono tutti i tuoi altri profili. Incrocia il risultato con il resto del fingerprint: un canvas stabile che però implica una GPU diversa da quella dichiarata dalle tue stringhe WebGL ha semplicemente spostato la contraddizione altrove.