Misurazione, non enumerazione
Non è mai esistita una web API che restituisca l'elenco dei font installati. Non ce n'è mai stato bisogno, perché misurare funziona quasi altrettanto bene e non richiede alcun permesso.
La tecnica è la stessa usata all'incirca dal 2007. Renderizza una stringa in un font di fallback noto e registra la larghezza. Poi renderizza la stessa stringa chiedendo per primo il font candidato, con lo stesso fallback dietro. Se la larghezza cambia, il candidato esiste ed è stato usato. Se non cambia, il candidato manca e ha disegnato di nuovo il fallback.
const span = document.createElement('span');
span.style.cssText = 'position:absolute;left:-9999px;font-size:72px';
span.textContent = 'mmmmmmmmmmlli';
span.style.fontFamily = 'monospace';
document.body.appendChild(span);
const base = span.offsetWidth;
span.style.fontFamily = '"Segoe UI", monospace';
const installed = span.offsetWidth !== base;
La stringa di test non è casuale. mmmmmmmmmmlli mescola i glifi più larghi e più stretti della maggior parte dei caratteri latini, massimizzando la differenza di larghezza fra due font qualsiasi. A 72 pixel la differenza è impossibile da non vedere.
Esegui tutto questo su un elenco di due o trecento candidati e hai il profilo dei font. Costa qualche decina di millisecondi e non lascia traccia in nessuna richiesta di permesso.
Perché i font identificano il sistema operativo
Gli insiemi di font non sono casuali. Arrivano con il sistema operativo, e ogni piattaforma ne distribuisce uno caratteristico.
| Font | Distribuito con | Cosa significa la sua presenza |
|---|---|---|
Segoe UI | Windows Vista e successivi | Windows, quasi con certezza |
Segoe UI Variable | Windows 11 | Distingue 11 da 10 |
Helvetica Neue | macOS | Piattaforma Apple |
Ubuntu, Liberation Sans | Distribuzioni Linux diffuse | Linux |
Calibri, Cambria | Microsoft Office | Office installato; debole segnale di Windows |
Arial, Times New Roman | Quasi ovunque | Niente |
È il controllo che smaschera gratis uno user agent falsificato. Un profilo che dichiara Windows 11 e non riesce a renderizzare Segoe UI Variable si è contraddetto in un solo confronto di offsetWidth — nessun dato esterno, nessun database di reputazione, nessun machine learning.
L'errore inverso è altrettanto comune e più difficile da vedere: un profilo che dichiara macOS su un host Windows, dove il Segoe UI dell'host è ancora installato e misurabile. Lo user agent dice Mac; i font dicono Windows; vincono i font.
Quanto vale
Il lavoro di Panopticlick della EFF ha stimato gli elenchi di font intorno a 13,9 bit di informazione identificativa — sufficienti da soli a restringere un browser a uno su circa quindicimila. È fra i valori più alti per un singolo segnale in tutta la letteratura sul fingerprinting, sopra la risoluzione dello schermo e ben sopra la stringa dello user agent.
Due avvertenze per restare onesti. Primo, quella cifra viene da una popolazione auto-selezionata di utenti interessati alla privacy e sovrastima l'entropia del web generale. Secondo, sta calando: Windows 11, le versioni recenti di macOS e le distribuzioni Linux più diffuse distribuiscono set predefiniti sempre più simili, e i web font hanno tolto quasi ogni motivo di installarne altri.
Quel che sopravvive al calo è la parte che qui conta. Anche se l'elenco dei font non identifica più una macchina in modo univoco, identifica comunque la piattaforma — e l'identificazione della piattaforma è ciò che smonta un profilo incoerente.
Gli altri modi di chiedere
La misurazione della larghezza è il metodo classico, non l'unico. Un'implementazione che aggancia solo offsetWidth si perderà questi.
measureText del canvas
ctx.measureText() restituisce un oggetto TextMetrics con la larghezza di avanzamento sub-pixel e, nei browser moderni, l'ascendente e il discendente reali del riquadro di ingombro. È più preciso della misurazione sugli elementi e vive nell'API canvas, quindi può girare dentro un OffscreenCanvas in un worker, dove gli hook a livello di pagina non arrivano.
Solo CSS
Il rilevamento dei font si può fare senza alcun JavaScript, usando @font-face con unicode-range e una richiesta di immagine di sfondo che parte solo quando viene usato un certo fallback. Nessuno script da agganciare, nessuna API da sovrascrivere.
Local Font Access
Chrome 103 ha introdotto window.queryLocalFonts(), che restituisce direttamente l'elenco completo dei font locali — dietro una richiesta di permesso all'utente. In pratica gli script di fingerprinting non la usano, proprio perché chiede il permesso. Vale la pena conoscerla soprattutto perché un browser che dichiara Chrome 147 dovrebbe avere l'API presente, e un profilo che l'ha rimossa ha creato una nuova incoerenza.
Farlo nel modo giusto
Funzionano due approcci, e uno molto comune no.
Cosa non funziona: restituire un elenco vuoto o minimo. Un browser con cinque font è più notevole di uno con duecento. È lo stesso errore della randomizzazione del canvas: hai sostituito un valore comune con uno raro e l'hai chiamato privacy.
Primo approccio: distribuire il set giusto. Il profilo dichiara Windows 11, quindi l'elenco dei font è quello predefinito di Windows 11, presente e misurabile. È pienamente realizzabile solo quando lo stack dei font è sotto controllo — il browser deve risolvere le richieste di font su un set dichiarato invece che su quello installato sull'host. È una questione a livello di engine, ed è per questo che è una delle cose più difficili da aggiungere a posteriori.
Secondo approccio: non dichiarare una piattaforma su cui non sei. Esegui i profili Windows su Windows e quelli Mac su Mac. Poco glamour, del tutto efficace e gratis. Se puoi scegliere l'host, è la risposta corretta più economica a disposizione.
Qualunque strada tu prenda, vale il vincolo dell'articolo sulla coerenza: il set di font deve concordare con lo user agent, con il renderer WebGL e con la stringa della piattaforma. I font sono semplicemente i più economici dei quattro da verificare, ed è per questo che vengono verificati per primi.
Controllare il proprio
Il test più rapido è una singola sonda contro il sistema operativo che dichiari. Incolla questo nella console di un profilo e leggi la risposta:
function has(f) {
const s = document.createElement('span');
s.style.cssText = 'position:absolute;left:-9999px;font-size:72px';
s.textContent = 'mmmmmmmmmmlli';
document.body.appendChild(s);
s.style.fontFamily = 'monospace'; const base = s.offsetWidth;
s.style.fontFamily = `"${f}", monospace`; const w = s.offsetWidth;
s.remove();
return w !== base;
}
console.log({
windows: has('Segoe UI'),
win11: has('Segoe UI Variable'),
macos: has('Helvetica Neue'),
linux: has('Ubuntu')
});
Esattamente uno di questi dovrebbe risultare vero, e dev'essere quello che corrisponde al tuo user agent. Due valori veri significano che il tuo profilo indossa due sistemi operativi insieme. Per un quadro più completo, browserleaks.com/fonts e CreepJS enumerano entrambi il set completo e, nel caso di CreepJS, ti dicono a chiare lettere quando l'elenco non concorda con la piattaforma che dichiari.