Browser Fingerprinting in 2026: What Still Works, What Doesn't
Browser fingerprinting still works in 2026, but the useful techniques have shifted underneath everyone. Chrome's Privacy Sandbox has frozen or removed half the signals fingerprinting libraries depended on. Firefox and Safari added noise injection and API restrictions. Brave blocks attempts outright. Meanwhile headless browser frameworks have gotten dramatically better at spoofing what remains. If you're building or maintaining a fingerprinting system, a lot of the advice from even two years ago is obsolete. This is a technical audit of what still produces usable signal, what's been neutralized, and what emerged to replace the losses. One caveat up front: a fingerprint is not a durable cross-browser identity, and no single fingerprint should be treated as proof that a visitor is human or automated. The Scoreboard Each technique rated on two axes - entropy (how much identifying information it produces) and durability (how resistant it is to spoofing and browser mitigation). | Technique | Entropy | Durability | Status in 2026 | |---|---|---|---| | User-Agent string | Very low | None | Dead. Frozen by Chrome 107+. | | navigator.plugins | None | None | Dead. Returns empty array in Chrome. | | navigator.platform | Very low | None | Dead. Frozen to generic values. | | Canvas fingerprint | Medium | Medium | Degraded but usable. Noise in Firefox/Brave. | | WebGL fingerprint | Medium-High | Medium-High | Still strong. Renderer strings remain diverse. | | WebGL rendering | Medium | Medium | Works. GPU output is hard to standardize. | | AudioContext | Medium | Medium | Works. Hardware timing differences persist. | | Font enumeration | Low-Medium | Low | Declining. OS font standardization. | | Screen/display props | Low | Low | Minimal entropy. Heavily spoofed. | | Client Hints | Low | Low | Reduced by design. | | TLS fingerprint (JA4) | High | Very High | Strongest signal. Cannot be spoofed from JS. | | HTTP/2 settings | Medium | High | Underutilized. Good connection-level entropy. | | TCP/IP stack | Low-Medium | High | Niche but durable. | The trend is clear: JavaScript-accessible signals are eroding. Network-level signals are ascendant. The most durable techniques in 2026 operate below the browser's API surface, where privacy extensions and stealth plugins can't reach them. What's Dead User-Agent string. Chrome killed it. Since Chrome 107 in late 2022 the string is frozen - the version number still increments, but OS version is pinned to Windows NT 10.0 , platform details are generic, and it no longer differentiates minor versions or OS builds. Firefox and Safari followed. You can distinguish Chrome from Firefox from Safari, and that's about it. For bot detection it's worse than useless, because every automation framework sets whatever string it wants. navigator.plugins / navigator.mimeTypes . Used to return arrays of installed plugins - a user with Flash 32.0.0.453, Java 8u281 and Chrome PDF Viewer had a distinct, slowly-changing signature. Chrome now returns a fixed generic array. Firefox the same. Remove these from any library that still checks them. navigator.platform . Frozen to generic values like Win32 regardless of actual architecture. The Client Hints replacement (navigator.userAgentData.platform ) is designed to be lower entropy and gates detailed values behind a permission request. What's Degraded but Usable Canvas fingerprinting Draw a complex scene, read back the pixel data, hash it. GPU hardware, driver versions, font rendering and anti-aliasing produce slightly different output across devices. Per-browser reality in 2026: - Chrome - still consistent, device-specific output. No noise injection. Highest-fidelity target. - Firefox - noise injection since 113 via privacy.resistFingerprinting . Off by default, on in strict privacy mode and private windows. When enabled, the same device produces a different hash on every page load. - Safari - minimal canvas protection. Still stable. - Brave - aggressively randomizes by default. Effectively useless against Brave users. function getCanvasFingerprint() { const canvas = document.createElement('canvas'); canvas.width = 256; canvas.height = 256; 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 fingerprint', 2, 15); ctx.fillStyle = 'rgba(102, 204, 0, 0.7)'; ctx.fillText('Browser fingerprint', 4, 17); // Geometric shapes for GPU-dependent rendering ctx.beginPath(); ctx.arc(50, 50, 50, 0, Math.PI * 2, true); ctx.closePath(); ctx.fill(); return canvas.toDataURL(); } Verdict: still works on roughly 80% of browsers. Expect continued degradation. Font enumeration Weakened for three reasons: OS standardization (Windows 11, recent macOS and modern Linux distros ship increasingly similar default sets), web font dominance (most modern sites never trigger system font rendering), and browser restrictions (privacy.resistFingerprinting returns a fixed list). Still distinguishes Windows from macOS from Linux. The days of font lists as a high-entropy identifier are over. Screen and display properties A 1920×1080 display at 1× describes tens of millions of devices. Trivially spoofed - Playwright and Puppeteer set arbitrary viewport sizes in one line. Include in a composite, don't rely on it. What Still Works WebGL - the quiet workhorse Two levels. Parameter enumeration exposes hardware and driver information: function getWebGLFingerprint() { const canvas = document.createElement('canvas'); const gl = canvas.getContext('webgl'); if (!gl) return null; const debugInfo = gl.getExtension('WEBGL_debug_renderer_info'); return { vendor: gl.getParameter(gl.VENDOR), renderer: gl.getParameter(gl.RENDERER), unmaskedVendor: debugInfo ? gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL) : null, unmaskedRenderer: debugInfo ? gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL) : null, maxTextureSize: gl.getParameter(gl.MAX_TEXTURE_SIZE), maxViewportDims: gl.getParameter(gl.MAX_VIEWPORT_DIMS), extensions: gl.getSupportedExtensions(), shadingLanguageVersion: gl.getParameter(gl.SHADING_LANGUAGE_VERSION), }; } The unmasked renderer string alone carries substantial entropy - ANGLE (NVIDIA GeForce RTX 4070 Ti Direct3D11 vs_5_0 ps_5_0) identifies a specific GPU model. Level 2 is render output: drawing a 3D scene and reading back pixels, with higher variability than canvas because 3D pipelines differ more across GPU architectures. Why it's durable: browser vendors have been reluctant to restrict WebGL because doing so breaks legitimate applications - games, data visualizations, 3D product viewers, mapping. Injecting noise would break these visibly. For bot detection specifically, WebGL is gold. Headless Chrome in a cloud VM reports the VM's virtual GPU - typically Google SwiftShader or llvmpipe - instantly distinguishable from any real user GPU. Even BaaS platforms that spoof this value struggle to replicate the full constellation of parameters a real GPU produces. AudioContext Exploits hardware-dependent differences in audio signal processing. Route an OscillatorNode through a DynamicsCompressorNode , read the output, and you get floating-point sample values that vary with the audio hardware and driver stack: function getAudioFingerprint() { return new Promise((resolve) => { const context = new OfflineAudioContext(1, 44100, 44100); const oscillator = context.createOscillator(); oscillator.type = 'triangle'; oscillator.frequency.setValueAtTime(10000, context.currentTime); const compressor = context.createDynamicsCompressor(); compressor.threshold.setValueAtTime(-50, context.currentTime); compressor.knee.setValueAtTime(40, context.currentTime); compressor.ratio.setValueAtTime(12, context.currentTime); compressor.attack.setValueAtTime(0, context.currentTime); compressor.release.setValueAtTime(0.25, context.currentTime); oscillator.connect(compressor); compressor.connect(context.destination); oscillator.start(0); context.startRendering().then((buffer) => { const data = buffer.getChannelData(0); let sum = 0; for (let i = 4500; i < 5000; i++) sum += Math.abs(data[i]); resolve(sum); }); }); } Lower entropy than WebGL, but it's an independent signal that's hard to spoof because it depends on the actual audio processing pipeline rather than a JavaScript property. Headless browsers often have no audio stack at all, or a software implementation producing output that matches no real desktop configuration. TLS fingerprinting (JA4) The biggest shift in fingerprinting since canvas was discovered. TLS fingerprinting doesn't operate in JavaScript at all. It analyzes the ClientHello sent during the HTTPS handshake - before page content loads, before JavaScript executes, before any browser API can be manipulated. The ClientHello contains supported cipher suites in preference order, TLS extensions and their order, supported groups, signature algorithms, and ALPN protocols. JA4 hashes these into a fingerprint identifying the TLS stack implementation. Critically: you cannot change your JA4 fingerprint from JavaScript. It's determined by the TLS library compiled into the client. Which means: - A Playwright bot claiming Chrome 126 but running an older Chromium has a JA4 that doesn't match real Chrome 126. - A Python requests session spoofing a Chrome User-Agent has a JA4 matchingurllib3 , not Chrome. - A BaaS platform running headless Chrome in the cloud has a JA4 matching their specific Chromium build - often months behind stable. The cost: it requires server-side or proxy-level access to the raw handshake. You can't do it from JavaScript. But if you control the server, or use a CDN that exposes TLS metadata (Cloudflare exposes JA3 and JA4 in firewall rules), it's the most powerful identification signal available. HTTP/2 settings When a client opens an HTTP/2 connection it sends a SETTINGS frame - initial window size, max concurrent streams, header table size, enabled push. Different implementat
Comments
No comments yet. Start the discussion.