Saltar al contenido principal
en/blog/obscura/research/canvas-webgl-fingerprinting/

obscura/research/canvas-webgl-fingerprinting

3 min read

Overview

Canvas fingerprinting exploits subtle differences in how hardware and software render graphics. These differences arise from:

  • GPU model and driver version
  • Operating system font rendering (anti-aliasing, hinting, subpixel rendering)
  • Browser rendering engine
  • Installed system fonts

Canvas Fingerprinting

Mechanism

  1. Site draws shapes and text on an HTML5 <canvas> element
  2. Calls canvas.toDataURL() to extract pixel data as base64
  3. Hashes the result (MD5, SHA-1, or custom hash)
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
ctx.font = '14px Arial'
ctx.fillStyle = '#f60'
ctx.fillRect(125, 1, 62, 20)
ctx.fillStyle = '#069'
ctx.fillText('BrowserLeaks,com <canvas> 1.0', 2, 15)

const fingerprint = canvas.toDataURL()
const hash = md5(fingerprint) // unique per device

Entropy Sources

Source Variability
Font rendering (anti-aliasing, hinting) High
Subpixel rendering order (RGB vs BGR) Medium
GPU rasterization of shapes Medium
Color space conversion Low

WebGL Fingerprinting

Mechanism

WebGL exposes the GPU directly via:

const gl = canvas.getContext('webgl')
const debugInfo = gl.getExtension('WEBGL_debug_renderer_info')
const renderer = debugInfo.UNMASKED_RENDERER_WEBGL
// e.g., "NVIDIA GeForce RTX 3080/PCIe/SSE2"
const vendor = debugInfo.UNMASKED_VENDOR_WEBGL
// e.g., "NVIDIA Corporation"

Additionally, rendering 3D scenes produces hardware-specific pixel output.

Signals

  • UNMASKED_RENDERER_WEBGL exact GPU model
  • UNMASKED_VENDOR_WEBGL GPU vendor
  • WebGL shader precision
  • MAX_TEXTURE_SIZE, MAX_VERTEX_ATTRIBS, etc.
  • Render of 3D scene (hash of pixels)

WebGPU Fingerprinting

WebGPU (the successor to WebGL) exposes even more granular information:

const adapter = await navigator.gpu.requestAdapter()
const info = await adapter.requestAdapterInfo()
// { vendor: "nvidia", architecture: "ampere", device: "..." }

Signals

  • adapterInfo.vendor
  • adapterInfo.architecture
  • adapterInfo.device
  • Feature set (which GPU features are supported)

Why Proxy-Level Spoofing is Hard

Canvas/WebGL/WebGPU rendering happens on the local GPU. The proxy only sees the network traffic. By the time the data reaches the proxy, the rendering is already complete.

Possible mitigation approaches:


  1. Block the API: Make WebGL return null, canvas throw an error. Problem: detectable as a signal ("WebGL blocked" is itself a fingerprint trait).


  2. Inject JS noise: CanvasBlocker (Firefox) randomizes canvas output by injecting a random pixel before toDataURL(). Problem: if the site reads canvas 3 times and gets 3 different hashes, it detects tampering. Also requires frequency-consistent noise.

  3. Image data override via Proxy: Override CanvasRenderingContext2D.prototype.getImageData via injected JS to return generic data. Problem: only works on getImageData, not toDataURL. Multiple paths to extract pixels.


  4. Software GPU rendering (SwiftShader/Lavapipe): Replace GPU rendering with CPU-based deterministic renderer. Problem: massive performance cost, no 3D acceleration.


Formulas

Canvas Fingerprint Entropy

Hcanvas=i=1nP(xi)log2P(xi)H_{canvas} = -\sum_{i=1}^{n} P(x_i) \log_2 P(x_i)

Where P(xi)P(x_i) is the probability of a given hash across different devices.

Estimated entropy: ~5-7 bits (can distinguish ~32-128 devices).

Uniqueness Probability

P(unique)=1i=1kNiNP(\text{unique}) = 1 - \prod_{i=1}^{k} \frac{N - i}{N}

Where NN is the population size and kk is the number of distinct canvas outputs in the sample.

Conclusions

  • Canvas/WebGL fingerprinting cannot be fully spoofed at the proxy level
  • Noise injection via JS is partial mitigation but detectable
  • Blocking these APIs is detectable and becomes a signal itself
  • The only near-complete solution is Tor Browser's approach (normalize all users to identical output)
  • For Obscura: best approach is injected JS that adds consistent noise + blocks WEBGL_debug_renderer_info