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
- Site draws shapes and text on an HTML5
<canvas>element - Calls
canvas.toDataURL()to extract pixel data as base64 - 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_WEBGLexact GPU modelUNMASKED_VENDOR_WEBGLGPU 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.vendoradapterInfo.architectureadapterInfo.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:
Block the API: Make WebGL return null, canvas throw an error. Problem: detectable as a signal ("WebGL blocked" is itself a fingerprint trait).
-
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. -
Image data override via Proxy: Override
CanvasRenderingContext2D.prototype.getImageDatavia injected JS to return generic data. Problem: only works ongetImageData, nottoDataURL. Multiple paths to extract pixels.
Software GPU rendering (SwiftShader/Lavapipe): Replace GPU rendering with CPU-based deterministic renderer. Problem: massive performance cost, no 3D acceleration.
Formulas
Canvas Fingerprint Entropy
Where is the probability of a given hash across different devices.
Estimated entropy: ~5-7 bits (can distinguish ~32-128 devices).
Uniqueness Probability
Where is the population size and 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