La defensa que se vuelve en tu contra
El fingerprinting del canvas funciona porque la rasterización por GPU no está estandarizada a nivel de píxel. Pide a dos máquinas que dibujen el mismo texto con la misma fuente y el mismo tamaño y el suavizado diferirá por un pelo. Haz el hash de los píxeles y obtienes un valor estable en un dispositivo y distinto en el siguiente.
La contramedida intuitiva es añadir ruido: alterar unos cuantos píxeles antes de calcular el hash, para que el valor cambie y no sirva para rastrearte. Casi todos los navegadores antidetect lo ofrecen, normalmente con la etiqueta «canvas: noise».
El problema es que el hash del canvas de un navegador real no cambia. Misma máquina, mismo navegador, misma página, mismo resultado — hoy, mañana y tras un reinicio. Esa estabilidad es justo la razón por la que merece la pena recoger la señal.
Así que un valor que cambia en cada lectura no se ha vuelto anónimo. Se ha vuelto anómalo. Es el mismo tipo de error que poner navigator.webdriver = false: la respuesta honesta te identifica, y la deshonesta te identifica como alguien que miente.
Cómo funciona de verdad la detección
Detectar la aleatorización no requiere nada ingenioso. Cuatro métodos, más o menos ordenados por lo a menudo que aparecen en la práctica:
Dibujar lo mismo dos veces
Renderizar un canvas idéntico dos veces en la misma carga de página y hashear ambos. Navegador real: idénticos. Ruido por llamada: distintos. Son dos líneas de código y están en todo script de detección serio.
Promediar el ruido hasta que desaparece
Si el ruido es aleatorio y de media cero, renderizar n veces y promediar empuja el resultado hacia el valor verdadero a un ritmo de aproximadamente √n. Veinte renderizados suelen bastar para recuperar el hash subyacente. La aleatorización le ha costado al rastreador unos milisegundos y a ti tu anonimato igualmente — encima marcándote como alguien que aleatoriza.
Comprobar si ese ruido encaja
Un canvas aleatorizado no es sospechoso por sí mismo. Brave lo trae de serie — su «farbling» deriva el ruido de una semilla por sesión y por origen. Tor Browser pregunta en su lugar. Lo que importa es si el ruido encaja con el navegador que dices ser. Chrome no aleatoriza el canvas. Un cliente cuyo user agent dice Chrome 147 y cuyo canvas cambia en cada llamada se ha contradicho, y la contradicción es el hallazgo, no el ruido.
Variar la ruta de renderizado
Las implementaciones ingenuas enganchan toDataURL() y getImageData(). Hay otras salidas: toBlob(), createImageBitmap(), el readPixels() de WebGL, OffscreenCanvas dentro de un worker. Si una ruta lleva ruido y otra no, las dos no cuadran — y ese desacuerdo es una prueba más fuerte que cualquiera de los dos valores por separado.
El camino de los workers en particular
Este último merece nota aparte, porque es donde se rompen casi todas las implementaciones.
OffscreenCanvas puede transferirse a un Web Worker y renderizarse allí. El worker tiene su propio ámbito global y su propia copia de las APIs de canvas, y una inyección de script a nivel de página no lo alcanza. Si tu ruido lo aplica JavaScript inyectado, el canvas del worker sale limpio.
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
Ahora la página tiene dos hashes de canvas del mismo dispositivo que no coinciden. No existe configuración legítima en la que eso ocurra. Es el mismo problema estructural descrito en el artículo sobre CDP: lo que se aplica desde dentro de la página no puede alcanzar un contexto que la página no posee.
Cómo es una implementación correcta
El objetivo no es «un valor distinto cada vez». Es un valor distinto al de los demás, y el mismo valor siempre para mí. Dicho así, los requisitos salen solos:
| Propiedad | Requisito |
|---|---|
| Dentro de una misma carga de página | Idéntico en cada renderizado |
| Entre sesiones | Idéntico para el mismo perfil, siempre |
| Entre perfiles | Distinto, y no por un desplazamiento constante |
| Entre rutas de renderizado | 2D, WebGL, OffscreenCanvas y worker coinciden |
| Verosimilitud | Coherente con la GPU que el perfil declara |
El mecanismo que satisface los cinco es una semilla. Fija una semilla aleatoria al crear el perfil, guárdala y deriva de ella la alteración de los píxeles de forma determinista. Cada renderizado de ese perfil produce la misma salida porque la semilla es la misma. Dos perfiles difieren porque difieren sus semillas. Y como la derivación ocurre donde se producen los píxeles — en el rasterizador, no en un método JavaScript enganchado —, todas las rutas reciben el mismo trato, workers incluidos.
Ese último punto es la razón de que esto sea difícil de hacer bien fuera de un motor modificado. No hay manera soportada de llegar al rasterizador de un worker desde un script de contenido.
El argumento para no tocarlo
Existe un argumento real, que merece exponerse con justicia, para no tocar el canvas en absoluto.
Si usas un perfil por máquina física, tu hash de canvas auténtico es un valor perfectamente corriente de un ordenador perfectamente corriente. Es estable, coherente en todas las rutas y encaja con tu GPU porque es tu GPU. Nada en él invita a mirar dos veces.
Eso deja de funcionar en el momento en que arrancas un segundo perfil en la misma máquina. Ahora los dos perfiles producen el mismo hash, y cualquier plataforma que haga fingerprinting del canvas puede relacionarlos — justo el resultado que querías evitar.
Así que la decisión no es «ruido o no ruido». Es:
- Una identidad por máquina — deja el canvas en paz. Hardware real, hash real, nada que explicar.
- Varias identidades por máquina — necesitas valores de canvas por perfil, y deben ser estables y coherentes. La aleatorización por llamada es la peor de las tres opciones, porque ni te esconde ni impide la vinculación.
Pruébalo tú mismo
No hace falta un proveedor de detección para comprobarlo. Abre la consola de tu navegador en cualquier página y ejecuta:
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());
Tres cadenas idénticas es lo que da un navegador real. Tres cadenas distintas significan ruido por llamada, y significan que cualquier web que ejecute esas mismas tres líneas lo sabe.
Luego ejecútalo otra vez tras reiniciar el perfil. El valor debería ser el mismo de antes si quieres que el perfil parezca un dispositivo y no un blanco móvil — y distinto del que producen tus otros perfiles. Contrasta el resultado con el resto del fingerprint: un canvas estable que implica una GPU que tus cadenas de WebGL no declaran solo ha movido la contradicción a otro sitio.