WebGL Fingerprinting: How It Works and How to Defend Against It

**TL;DR:** WebGL fingerprinting identifies your browser by reading exact GPU details (vendor, renderer, supported extensions, shader precision) and by hashing pixel-level differences in how your GPU plus driver render a known 3D scene. It survives cookie clearing, private mode, and VPN switches, and adds roughly 10 to 13 bits of entropy on its own. ## What WebGL fingerprinting actually is WebGL is a JavaScript API that gives web pages direct access to your GPU through OpenGL ES. It was added to browsers so games, maps, and 3D viewers could render hardware-accelerated graphics without a plugin. The same API surface that lets Google Maps draw 3D buildings also lets any script on any page interrogate your graphics stack at a level of detail that older fingerprinting techniques cannot reach. A WebGL fingerprint is two things stitched together: 1. **A parameter dump.** The script asks the WebGL context for vendor and renderer strings, maximum texture size, shader precision, supported extensions, and a few dozen other constants. These are deterministic across page loads on the same machine. 2. **A render hash.** The script draws a specific 3D scene off-screen, reads the rendered pixels back, and hashes them. Two different GPU plus driver combinations produce subtly different output for the same shader code. The hash is stable per machine and effectively unique per GPU plus driver version pair. If you understood our earlier piece on [canvas fingerprinting](/blog/canvas-fingerprinting-explained), WebGL is the same idea pushed one layer deeper. Canvas exploits the 2D rasterizer and font stack. WebGL exploits the GPU itself. ## How the attack works There are three distinct probes a fingerprinting script will run. Each one is cheap, silent, and requires no permissions. ### Probe 1: parameter readout The simplest leak is `WebGLRenderingContext.getParameter()`. The spec exposes vendor and renderer through `gl.VENDOR` and `gl.RENDERER`, but for security reasons those return generic strings like `WebKit` and `WebKit WebGL` in modern browsers. The real GPU model is hidden behind the [WEBGL_debug_renderer_info](https://registry.khronos.org/webgl/extensions/WEBGL_debug_renderer_info/) extension, which was originally intended for debugging and is still enabled by default in Chrome, Edge, and unhardened Firefox. ```javascript const canvas = document.createElement('canvas'); const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl'); // Generic strings - usually masked. const vendor = gl.getParameter(gl.VENDOR); const renderer = gl.getParameter(gl.RENDERER); // The actual leak - GPU model in plain text. const ext = gl.getExtension('WEBGL_debug_renderer_info'); if (ext) { const unmaskedVendor = gl.getParameter(ext.UNMASKED_VENDOR_WEBGL); const unmaskedRenderer = gl.getParameter(ext.UNMASKED_RENDERER_WEBGL); console.log(unmaskedVendor, unmaskedRenderer); // e.g. "Google Inc. (NVIDIA)" "ANGLE (NVIDIA, NVIDIA GeForce RTX 3070 Direct3D11 vs_5_0 ps_5_0, D3D11)" } ``` That single string usually pins down your GPU model, the graphics backend (Direct3D 11 on Windows, Metal on macOS, ANGLE on Linux), the driver vendor, and on some systems even the shader compiler version. On a desktop with a discrete GPU it is close to a serial number. ### Probe 2: capability matrix Even with `WEBGL_debug_renderer_info` blocked, dozens of parameters are still readable and vary across hardware: ```javascript const params = [ 'MAX_TEXTURE_SIZE', 'MAX_CUBE_MAP_TEXTURE_SIZE', 'MAX_RENDERBUFFER_SIZE', 'MAX_VIEWPORT_DIMS', 'MAX_VERTEX_ATTRIBS', 'MAX_VERTEX_UNIFORM_VECTORS', 'MAX_FRAGMENT_UNIFORM_VECTORS', 'MAX_VARYING_VECTORS', 'ALIASED_LINE_WIDTH_RANGE', 'ALIASED_POINT_SIZE_RANGE', 'RED_BITS', 'GREEN_BITS', 'BLUE_BITS', 'DEPTH_BITS', ]; const profile = {}; for (const p of params) profile[p] = gl.getParameter(gl[p]); // Shader precision per stage and type. for (const stage of ['VERTEX_SHADER', 'FRAGMENT_SHADER']) { for (const prec of ['HIGH_FLOAT', 'MEDIUM_FLOAT', 'LOW_FLOAT']) { const fmt = gl.getShaderPrecisionFormat(gl[stage], gl[prec]); profile[`${stage}_${prec}`] = [fmt.rangeMin, fmt.rangeMax, fmt.precision]; } } // Supported extension list - itself a fingerprint. profile.extensions = gl.getSupportedExtensions(); ``` The supported-extensions array alone is a meaningful entropy source. Apple Silicon GPUs expose a different list than Intel iGPUs, which expose a different list than AMD discrete cards. The shader-precision triplet differs across mobile and desktop because mobile GPUs commonly downgrade `HIGH_FLOAT` to mediump. ### Probe 3: render hash The strongest WebGL signal comes from actually rendering a scene and reading the pixels back. The classic recipe, similar to what fingerprintjs and the original [WebGL fingerprinting research](https://www.usenix.org/system/files/conference/usenixsecurity16/sec16_paper_laperdrix.pdf) by Laperdrix et al. used: ```javascript function webglHash() { const canvas = document.createElement('canvas'); canvas.width = 256; canvas.height = 128; const gl = canvas.getContext('webgl'); // A simple textured triangle with a non-trivial fragment shader. const vs = `attribute vec2 p; varying vec2 v; void main(){ v=p; gl_Position=vec4(p,0.,1.); }`; const fs = `precision mediump float; varying vec2 v; void main(){ float r = sin(v.x*30.0)*cos(v.y*30.0); gl_FragColor = vec4(r, v.x, v.y, 1.0); }`; const prog = gl.createProgram(); for (const [type, src] of [[gl.VERTEX_SHADER, vs], [gl.FRAGMENT_SHADER, fs]]) { const s = gl.createShader(type); gl.shaderSource(s, src); gl.compileShader(s); gl.attachShader(prog, s); } gl.linkProgram(prog); gl.useProgram(prog); const buf = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, buf); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1, 1,-1, -1,1, 1,1]), gl.STATIC_DRAW); const loc = gl.getAttribLocation(prog, 'p'); gl.enableVertexAttribArray(loc); gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0); gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); const pixels = new Uint8Array(256 * 128 * 4); gl.readPixels(0, 0, 256, 128, gl.RGBA, gl.UNSIGNED_BYTE, pixels); return sha256(pixels); } ``` The same shader, the same vertex data, the same viewport. The only thing that varies across machines is how your GPU plus driver implements `sin`, `cos`, IEEE-754 rounding, and the rasterizer interpolation. Those differences are tiny — often a single bit per pixel — but stable, and they ladder up into a stable 256-bit hash. A real fingerprinting library will run several of these scenes with different shader programs and combine the hashes. The most cited public corpus is the [Laperdrix dataset](https://amiunique.org), which has measured WebGL signals on hundreds of thousands of browsers. ## Why it is effective Independent measurement studies converge on similar numbers. The EFF [Panopticlick](https://coveryourtracks.eff.org) successor "Cover Your Tracks" project measures WebGL renderer string entropy at roughly 9 to 11 bits across its dataset. The Laperdrix 2016 paper measured a richer profile at 10 to 13 bits. The amiunique corpus shows that adding the render hash to the parameter dump pushes uniqueness above 95 percent on desktop. Translated to practical terms: WebGL alone divides the browser population into thousands of buckets. Combine it with a User-Agent string and screen resolution and you are looking at a near-unique identifier. Unlike cookies, it survives: - Clearing site data - Switching to private or incognito mode - Switching between VPN exits - Reinstalling the browser - Reinstalling the operating system, in most cases (the GPU model does not change) The only changes that actually move the WebGL fingerprint are a GPU swap, a driver update, or a browser version change that alters ANGLE behaviour. ## Defences that actually work ### Browser-level **Tor Browser and Mullvad Browser.** Both ship with WebGL gated behind a click-to-play prompt on default settings and with a long list of GPU-related APIs neutered. The render hash returns generic values, `WEBGL_debug_renderer_info` is blocked, and shader precision is normalized. If you want strong fingerprinting resistance with no configuration, [Mullvad Browser](https://mullvad.net/en/browser) is the path of least friction since it is Tor Browser minus the Tor network. **Firefox `privacy.resistFingerprinting`.** Set `privacy.resistFingerprinting` to true in `about:config` and Firefox blocks `WEBGL_debug_renderer_info`, returns spoofed parameter values, and randomizes the rendered pixel output. The same code path that powers Tor Browser. Side effect: timezone forced to UTC and screen size letterboxed to 1000x1000 buckets. **Firefox `webgl.disabled`.** The blunt option. Set `webgl.disabled` to true in `about:config` and the API returns null contexts. Breaks Google Maps 3D, Figma, any WebGL game, and a number of dashboards. **Brave farbling.** Brave randomizes WebGL output per-session per-eTLD+1, so two visits to the same site within the same session return identical hashes but a fresh session or a different site sees a fresh hash. This is the "Standard" shield default and is enough to defeat most off-the-shelf trackers, though it is detectable as Brave-specific behaviour. **LibreWolf.** Ships with `privacy.resistFingerprinting` on out of the box and Mozilla telemetry stripped. Good middle ground if you want the Firefox engine without flipping flags yourself. ### Extension-level - **CanvasBlocker (Firefox).** Despite the name, it covers WebGL too. Modes range from "ask permission" to "always randomize" to "block". Most flexible, but adds visible click prompts on the relaxed settings. - **Trace (Chromium and Firefox).** Mass-disables and spoofs around 30 fingerprinting APIs including WebGL parameters and renderer info. Lighter touch than CanvasBlocker. - **JShelter.** Academic project from the Free Software Foundation Europe. Adds noise to WebGL `readPixels` calls and spoofs the debug renderer extension. Aggressive defaults; breaks more sites but the strongest configurable defence in the extension space. ### Network-level WebGL fingerprinting happens entirely client-side. A VPN does nothing for it. Tor only helps because Tor Browser ships with the resist-fingerprinting code path enabled, not because of the network. This is one of the cleanest examples in the privacy stack of why "I use a VPN" is not a fingerprinting answer — see our piece on [Tor vs VPN](/blog/tor-vs-vpn-which-is-more-private) for the broader version of that argument. If you do still want a VPN for IP-layer privacy, audited no-logs providers like Mullvad and NordVPN handle that layer cleanly while you handle WebGL with a hardened browser. ### Tradeoffs to know going in Hardening WebGL has visible cost: - Google Maps 3D, Earth, and Street View break or fall back to 2D. - Figma, Miro, tldraw, and most WebGL-based design tools either fail to load or run on slow software fallback. - WebGL games (itch.io, in-browser emulators) refuse to start. - Some video conferencing tools that use WebGL for background blur lose that feature. For most developers the right setup is two browsers: a hardened daily driver (Mullvad Browser or LibreWolf) for general browsing and a vanilla browser kept open only for the WebGL-required tools. ## How to test if you are fingerprintable Run all three for a complete picture: - [privacyscore.dev](https://privacyscore.dev) runs the parameter readout, the render hash, and the two-shot randomization detector together and tells you whether your browser is leaking or defending. - [browserleaks.com/webgl](https://browserleaks.com/webgl) shows the raw values your browser exposes for every WebGL parameter and the actual rendered image hash. - [amiunique.org](https://amiunique.org) compares your full fingerprint, WebGL included, against its measured population. If the privacyscore.dev WebGL check shows two identical hashes from a back-to-back render, you are deterministic and trackable. If the two hashes differ, your browser is randomizing and you are protected. ## What about mobile **iOS Safari.** Apple disables `WEBGL_debug_renderer_info` by default and normalizes most parameter readouts. Render-hash entropy is meaningfully lower than on desktop because the GPU set is small (a handful of Apple Silicon SoCs) and the OS controls the driver. Lockdown Mode goes further and disables WebGL entirely along with JIT and several other attack surfaces. iOS Safari is the platform where WebGL fingerprinting is weakest by default. **Android Chrome.** The opposite story. `WEBGL_debug_renderer_info` is enabled, the GPU set is enormous (Adreno, Mali, PowerVR, Xclipse, Apple — across hundreds of SoC variants), and the render hash is highly distinguishing. Hardened alternatives on Android: Tor Browser for Android, Mull (Firefox fork with RFP), or Brave with shields on aggressive. **Mobile browsers built on system WebView.** Most third-party Android browsers that wrap the system WebView inherit Chrome's WebGL exposure. Switching to Firefox-based or Tor-based engines is the only real fix on Android. ## The bigger picture WebGL is one technique inside a larger family. The full fingerprinting surface as of 2026 includes: - [Canvas 2D rendering hash](/blog/canvas-fingerprinting-explained) — the pillar of this cluster. - WebGL parameters and render hash — this article. - Audio fingerprinting via the AudioContext oscillator pipeline — covered in a forthcoming post. - Font enumeration via `document.fonts` and Canvas text-width probes. - Client Hints (`Sec-CH-UA-*` headers) which the browser sends voluntarily but which leak high-fidelity device data. - Hardware sensors and battery API where exposed. Defending against one without defending against the others does not move your privacy score much. A browser that randomizes WebGL but leaks canvas is still trivially trackable. The right mental model is "fingerprinting resistance is a property of a complete browser configuration, not a single switch." ## FAQ **Does disabling WebGL actually help, or does the fact that I disabled it become its own fingerprint?** Disabling WebGL is itself a signal — but a low-entropy one, because plenty of browsers and users disable it. The signal "WebGL absent" puts you into a smaller bucket, not a unique one. Compared to a unique WebGL render hash, "WebGL absent" is a strict privacy improvement. **Can a VPN protect against WebGL fingerprinting?** No. WebGL fingerprinting runs entirely inside your browser. A VPN changes your IP address and nothing else relevant here. **Is incognito mode any defence?** No. Incognito clears cookies and local storage. Your GPU and driver do not change when you open a private window, so the WebGL fingerprint is identical. **Why does Tor Browser allow WebGL at all if it is so leaky?** Tor Browser allows it click-to-play with randomized output. The project decided full blocking broke too many legitimate sites. The randomization makes the leak non-tracking even when allowed. **Does using a virtual machine help?** Sometimes. The VM presents a virtualized GPU (often a Mesa or VMware vGPU) which is shared by millions of other VM users, putting you in a large bucket. But the host can still pass through GPU calls in some configurations, which leaks the host GPU. Worth testing with the tools above. **Will WebGL 2 or WebGPU make this worse?** Yes. WebGPU exposes more granular GPU capabilities than WebGL and is harder to normalize without breaking the API. Browser vendors are aware and shipping mitigations, but every new GPU-adjacent API is a new fingerprinting surface until it is hardened. **Is there any legitimate reason a site needs my unmasked GPU info?** A few. Map and game engines pick render paths based on GPU class. But the same engines work fine on the normalized strings Tor Browser and Mullvad Browser return — they just pick a conservative default. Sites that hard-fail without an unmasked renderer string are doing it for fingerprinting or for an unnecessary optimization. ## Action items 1. Run the WebGL probe on [privacyscore.dev](https://privacyscore.dev). If you score a deduction, decide whether to harden or to accept. 2. If you want strong defaults without configuration, switch your privacy-focused browsing to [Mullvad Browser](https://mullvad.net/en/browser) or LibreWolf. 3. If you are on Firefox already, flip `privacy.resistFingerprinting` to true and accept the side effects. 4. Read the [canvas fingerprinting post](/blog/canvas-fingerprinting-explained) next — it is the partner technique and the defences overlap. 5. While you are tightening the browser layer, also fix [WebRTC IP leaks](/blog/webrtc-ip-leak-fix), which sit at a different layer but are exposed on the same pages. Sources cited: Khronos [WEBGL_debug_renderer_info spec](https://registry.khronos.org/webgl/extensions/WEBGL_debug_renderer_info/), Laperdrix et al. [Beauty and the Beast: Diverting modern web browsers to build unique browser fingerprints](https://www.usenix.org/system/files/conference/usenixsecurity16/sec16_paper_laperdrix.pdf), [EFF Cover Your Tracks](https://coveryourtracks.eff.org), [MDN WebGL API](https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API), and the [amiunique.org](https://amiunique.org) live dataset.