DEV Community

ricco020
ricco020

Posted on

How browser fingerprinting works: canvas, WebGL and AudioContext explained

Browser fingerprinting is one of the most persistent tracking mechanisms on the web - and unlike cookies, you can't just "clear" it. Here's how it actually works under the hood, with real code.

What is browser fingerprinting?

When you visit a website, your browser leaks dozens of attributes: installed fonts, screen resolution, GPU model, audio stack behavior, canvas rendering quirks. Individually these seem harmless. Combined, they form a hash that's statistically unique to your device - a fingerprint.

The creepy part: fingerprinting survives incognito mode, VPNs, and cookie deletion.


1. Canvas fingerprinting

Canvas fingerprinting exploits the fact that different GPUs, drivers, and operating systems render the same drawing instructions slightly differently - different anti-aliasing, sub-pixel rendering, and color profiles produce subtly distinct pixel values.

function getCanvasFingerprint() {
  const canvas = document.createElement('canvas');
  const ctx = canvas.getContext('2d');

  ctx.textBaseline = 'top';
  ctx.font = '14px Arial';
  ctx.fillStyle = '#f60';
  ctx.fillRect(125, 1, 62, 20);
  ctx.fillStyle = '#069';
  ctx.fillText('Browser fingerprinting test', 2, 15);
  ctx.fillStyle = 'rgba(102, 204, 0, 0.7)';
  ctx.fillText('Browser fingerprinting test', 4, 17);

  return canvas.toDataURL();
}

async function hashString(str) {
  const buffer = new TextEncoder().encode(str);
  const hashBuffer = await crypto.subtle.digest('SHA-256', buffer);
  const hashArray = Array.from(new Uint8Array(hashBuffer));
  return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
}

const dataUrl = getCanvasFingerprint();
hashString(dataUrl).then(hash => console.log('Canvas hash:', hash.slice(0, 16)));
Enter fullscreen mode Exit fullscreen mode

Two machines running the same browser version but different GPUs will produce different dataUrl strings - and therefore different hashes.


2. WebGL fingerprinting

WebGL exposes your GPU model directly. WEBGL_debug_renderer_info is a notorious extension that returns the renderer and vendor strings:

function getWebGLFingerprint() {
  const canvas = document.createElement('canvas');
  const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');

  if (!gl) return { renderer: 'none', vendor: 'none' };

  const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');

  if (debugInfo) {
    return {
      renderer: gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL),
      vendor: gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL),
    };
  }

  return {
    renderer: gl.getParameter(gl.RENDERER),
    vendor: gl.getParameter(gl.VENDOR),
  };
}

console.log(getWebGLFingerprint());
// Example: { renderer: "ANGLE (NVIDIA GeForce RTX 3080...)", vendor: "Google Inc." }
Enter fullscreen mode Exit fullscreen mode

This alone narrows down your hardware significantly. Combined with the canvas hash, it's highly identifying.


3. AudioContext fingerprinting

Your audio stack introduces tiny floating-point rounding differences when processing audio signals. The AudioContext API exposes this:

async function getAudioFingerprint() {
  return new Promise((resolve) => {
    const AudioCtx = window.AudioContext || window.webkitAudioContext;
    if (!AudioCtx) return resolve('not_supported');

    const context = new AudioCtx();
    const oscillator = context.createOscillator();
    const analyser = context.createAnalyser();
    const gain = context.createGain();
    const scriptProcessor = context.createScriptProcessor(4096, 1, 1);

    gain.gain.value = 0; // silent - we only want the data

    oscillator.type = 'triangle';
    oscillator.frequency.value = 10000;
    oscillator.connect(analyser);
    analyser.connect(scriptProcessor);
    scriptProcessor.connect(gain);
    gain.connect(context.destination);

    scriptProcessor.onaudioprocess = (event) => {
      const inputData = event.inputBuffer.getChannelData(0);
      const fingerprint = inputData
        .reduce((acc, val) => acc + Math.abs(val), 0)
        .toString();
      resolve(fingerprint.slice(0, 20));
      oscillator.disconnect();
      scriptProcessor.disconnect();
      context.close();
    };

    oscillator.start(0);
  });
}

getAudioFingerprint().then(fp => console.log('Audio fingerprint:', fp));
Enter fullscreen mode Exit fullscreen mode

The accumulated float sum differs enough across hardware and OS audio drivers to contribute meaningful entropy to the overall fingerprint.


4. Font enumeration

Browsers don't expose a font list API directly, but you can infer installed fonts by measuring text rendering width with a fallback font:

function detectFont(fontName) {
  const baseFonts = ['monospace', 'sans-serif', 'serif'];
  const testString = 'mmmmmmmmmmlli';
  const testSize = '72px';

  const canvas = document.createElement('canvas');
  const ctx = canvas.getContext('2d');

  function measureWidth(font) {
    ctx.font = `${testSize} ${font}`;
    return ctx.measureText(testString).width;
  }

  const baseWidths = baseFonts.map(f => measureWidth(f));

  return baseFonts.some((base, i) => {
    const testWidth = measureWidth(`${fontName},${base}`);
    return testWidth !== baseWidths[i];
  });
}

const fontsToTest = ['Arial', 'Calibri', 'Futura', 'Gill Sans', 'Comic Sans MS', 'Impact', 'Palatino'];
const detectedFonts = fontsToTest.filter(detectFont);
console.log('Detected fonts:', detectedFonts);
Enter fullscreen mode Exit fullscreen mode

A user with Calibri and Futura installed is already in a much smaller group than average.


Why a VPN does not help

This is the most common misconception. A VPN changes your IP address. That's it.

Your canvas rendering, GPU model, installed fonts, and audio stack are entirely client-side. They have nothing to do with network routing. An attacker fingerprinting you via JavaScript sees the same values whether you're on your home ISP, a VPN endpoint, or Tor with standard browser settings.

To meaningfully resist fingerprinting you need:

  • Browser-level randomization - Firefox's privacy.resistFingerprinting, Brave's randomized canvas noise
  • Standardized GPU rendering - Tor Browser forces a fixed window size and disables WebGL
  • Font normalization - limiting available fonts to a small baseline set

Seeing it in action

A real fingerprinting library combines 15-30 such signals: screen resolution, timezone, navigator properties, touch support, hardware concurrency, device memory, and more - then hashes them into a single stable ID.

If you're curious about what your own browser is currently leaking, a tool that shows you your own browser fingerprint breaks it down signal by signal - canvas hash, WebGL renderer, audio entropy, font list - and gives you an overall uniqueness score. It's a useful reality check before assuming your incognito tab is actually private.


Reducing your fingerprint surface

Technique Effectiveness Trade-off
Brave Browser (randomized canvas) High Minor rendering artifacts
Firefox + resistFingerprinting High Some sites break
Tor Browser Very High Slow, limited usability
uBlock Origin Low (JS blocking only) Breaks many sites
VPN alone Near zero for fingerprinting False sense of security

The web platform keeps adding APIs, and each new one adds potential entropy. The gap between "private browsing" and actual privacy is significant - and understanding the mechanics is the first step to closing it.

Top comments (0)