Saltar al contenido principal
en/blog/obscura/research/web-fingerprinting/

obscura/research/web-fingerprinting

7 min read

Overview

Signals derived from web platform features, CSS capabilities, fonts, and other browser characteristics.


1. Font Fingerprinting

Mechanism

Fonts are enumerated via several techniques:

  1. CSS @font-face measurement: Create text, measure its width with a known font, then try loading a system font. If width changes, the font exists.

  2. Canvas measureText: Similar approach using canvas.

  3. document.fonts.check(): Modern API for font availability checking.


  4. Flash-based enumeration (legacy, still used): Flash can list all installed fonts.


Code Example

// Measurement-based font detection
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
const text = 'mmmmmmmmmmlli'

function detectFont(font) {
  ctx.font = `72px ${font}, monospace`
  return ctx.measureText(text).width
}

// Compare width with monospace baseline to detect

Entropy

Font list varies widely:

Platform Typical Fonts Uniqueness
Windows 11 stock ~80 fonts Medium
macOS Ventura+ ~120 fonts Medium
Linux (distro varies) 10-500 fonts Very High
Custom fonts installed Any Very High

Mitigation at Proxy

// Override font measurement to return consistent values
const originalMeasureText = CanvasRenderingContext2D.prototype.measureText
CanvasRenderingContext2D.prototype.measureText = function(text) {
  const result = originalMeasureText.call(this, text)
  // Round width to reduce font detection precision
  result.width = Math.round(result.width / 10) * 10
  return result
}

Limitation: Font detection is only reduced, not eliminated.


2. Browser Feature Support (25+ binary vectors)

Mechanism

Each browser supports a different subset of web APIs and CSS features. Collectively, this creates a browser+version fingerprint.

High-Entropy Features

Feature Chrome Firefox Safari
WebGL Yes Yes Yes
WebGPU Yes (flag) No No
WebUSB Yes No No
Web Bluetooth Yes No No
Web NFC Android No No
Web Authentication Yes Yes Yes
Web Assembly Yes Yes Yes
Service Workers Yes Yes Yes
Battery API No Yes No
Speech Synthesis Yes Yes Yes
GamePad API Yes Yes Yes (partial)
WebRTC Yes Yes Yes
CSS Grid Yes Yes Yes
CSS backdrop-filter Yes Yes Yes
CSS Container Queries Yes Yes Yes

Entropy

Each feature adds ~1 bit. With ~100 features tested, theoretical max is 100 bits, but features are highly correlated (minimal entropy beyond browser family and version).

Effective entropy: ~8-12 bits (~256-4096 combinations).

Mitigation

Cannot be done at proxy level these are inherent browser capabilities. Feature detection runs in JS before any injected script can run (and even if overridden, the browser still reports actual capabilities somewhere).


3. CSS Media Query Fingerprinting

Mechanism

CSS @media queries can brute-force screen dimensions, DPI, color depth, and other properties without JS:

@media (width: 1920px) { body::after { content: '1920' } }
@media (height: 1080px) { body::after { content: '1080' } }
@media (resolution: 1.5dppx) { body::after { content: '1.5' } }
@media (prefers-color-scheme: dark) { ... }
@media (prefers-reduced-motion: no-preference) { ... }
@media (hover: hover) { ... }
@media (pointer: fine) { ... }

Challenge for Proxy

CSS executes before any JS injection. The proxy can:

  1. Parse CSS in the response HTML and modify media queries
  2. Or rely on the fact that CSS-only fingerprint probes need to exfiltrate data (which needs JS or a network request)

Real mitigation: Block the exfiltration path (JS or image loading) rather than spoofing CSS values.

Media Query List

Query Values Leaked
width px Screen width
height px Screen height
aspect-ratio ratio Screen ratio
resolution dppx Pixel density
color bits Color depth
color-gamut srgb/p3/rec2020 Color space
prefers-color-scheme light/dark OS theme
prefers-reduced-motion no-preference/reduce OS setting
prefers-contrast no-preference/more/less OS setting
prefers-reduced-transparency no-preference/reduce OS setting
hover none/hover Input device
pointer none/coarse/fine Input device
any-hover none/hover Any input
any-pointer none/coarse/fine Any input
display-mode fullscreen/standalone PWA mode
prefers-color-gamut srgb/p3 Display gamut
dynamic-range standard/high HDR support
inverted-colors none/inverted OS accessibility
monochrome bits Monochrome display
forced-colors none/active Accessibility
prefers-reduced-data no-preference/reduce Data saver

4. ClientRects Fingerprinting

Mechanism

Element.getClientRects() returns slightly different values depending on OS, browser, and font rendering:

const span = document.createElement('span')
span.textContent = 'mmmmmmmmmmlli'
document.body.appendChild(span)
const rects = span.getClientRects()
// rects[0].width varies by OS/browser/font

Mitigation

Element.prototype.getClientRects = function() {
  const rects = Element.prototype.__proto__.getClientRects.call(this)
  return Array.from(rects).map(r => ({
    ...r,
    width: Math.round(r.width / 10) * 10,
    height: Math.round(r.height / 10) * 10
  }))
}

5. Web Audio API Fingerprint

See audio-fingerprinting.md for details.


6. WebGPU Fingerprinting

WebGPU provides structured device info not previously available:

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

// Adapter limits expose specific GPU capabilities
const limits = adapter.limits
// maxTextureDimension2D, maxStorageBufferBindingSize, etc.

// Feature set
adapter.features.has('texture-compression-bc')
adapter.features.has('shader-f16')

Exposed Data

Field Example Identifiability
vendor "nvidia", "apple", "amd", "intel" Low (4-5 values)
architecture "ampere", "rdna2", "tigerlake" Medium
device "integrated-graphics", "Apple M3" Medium
driver "460.32.03" High
backend "Metal", "Vulkan", "D3D12" Low
type "discrete-gpu", "integrated-gpu", "cpu" Low
memoryHeaps size and count High
limits 50+ capability values Very High

Mitigation

// Block WebGPU adapter info
navigator.gpu.requestAdapter = async function() {
  return null  // or return a fake minimal adapter
}

Cost: Blocks WebGPU entirely, which breaks some apps.


7. Service Worker Persistence

Mechanism

Service Workers can be used to identify users across sessions:

// Detect if a SW is installed
navigator.serviceWorker.getRegistrations().then(regs => {
  if (regs.length > 0) console.log('SW found')
})

Mitigation

navigator.serviceWorker.getRegistrations =
  async () => []

8. WebGL Extensions Fingerprint

WebGL exposes a list of supported extensions that varies by GPU, driver, and browser:

Extension Type Count Example
Standard WebGL ~30 OES_texture_float
WEBGL_* ~15 WEBGL_debug_renderer_info
EXT_* ~15 EXT_texture_filter_anisotropic
Browser-specific ~5 MOZ_WEBGL_*

Entropy: The exact set and count of extensions creates a hash.

Mitigation:

const originalGetExtension = WebGLRenderingContext.prototype.getExtension
WebGLRenderingContext.prototype.getExtension = function(name) {
  if (name === 'WEBGL_debug_renderer_info') return null
  if (name.startsWith('MOZ_')) return null
  return originalGetExtension.call(this, name)
}

Conclusions for Obscura

Vector Proxy Control Mitigation Strategy
Font enumeration Partial Round measurements, block Flash
Feature support None Accept inherent to browser
CSS media queries Partial Block exfiltration (DNS/network)
ClientRects Partial Round values via JS injection
WebGPU Partial Block adapter info
Service Workers Partial Override registration APIs
WebGL extensions Partial Filter extension list