Privacy Tools Guide

CSS fingerprinting detects installed fonts, screen resolution, and browser capabilities through stylesheet cascading and HTTP requests, enabling tracking without JavaScript by observing which CSS rules the browser applies. Trackers use @font-face to detect fonts, CSS media queries to identify screen properties, and selectors that trigger requests only when specific conditions match. Defend against CSS fingerprinting by disabling web fonts (use system fonts), blocking external stylesheet resources with privacy extensions, or using browsers with CSS fingerprinting protections built-in.

The Fundamental Mechanism

CSS fingerprinting exploits the cascade and specificity rules of stylesheets to detect user characteristics without executing any script. When a browser renders a page, it applies styles based on device type, screen resolution, installed fonts, and browser preferences. By observing which styles activate, trackers infer unique device properties that collectively form a fingerprint.

The technique relies on CSS selectors that match specific conditions. A stylesheet can include rules that only apply under certain circumstances—such as a particular font being available or a specific display configuration being present. When these conditions evaluate to true, the browser applies corresponding styles, and JavaScript-free tracking occurs through resource requests or computed style observations.

Detecting Installed Fonts

Font detection constitutes one of the most powerful CSS fingerprinting techniques. Different operating systems and applications ship with varying font collections, creating a unique signature per installation. The classic approach uses a fallback chain measurement:

@font-face {
  font-family: 'TestFont';
  src: local('Helvetica'), local('Arial'), local('sans-serif');
}

.font-test {
  font-family: 'TestFont', 'Courier New', monospace;
  font-size: 72px;
}

By measuring the width of rendered text containing the test font versus a fallback font, trackers determine which fonts exist on the system. A more direct method uses the font-display property to trigger network requests only when a specific font loads:

@font-face {
  font-family: 'DetectArial';
  src: url('/tracker?font=Arial');
}

@font-face {
  font-family: 'DetectHelvetica';
  src: url('/tracker?font=Helvetica');
}

Each successful request indicates the presence of that specific font, building a font-profile fingerprint unique to the visitor.

Screen and Device Property Detection

CSS media queries expose substantial device information without JavaScript execution. The @media rule triggers stylesheet sections based on hardware characteristics:

@media (min-width: 1920px) {
  body::after {
    content: '1920';
    display: none;
  }
}

@media (min-width: 2560px) {
  body::after {
    content: '2560';
    display: none;
  }
}

Beyond viewport dimensions, CSS exposes color depth, aspect ratio, and pointer characteristics. The resolution media query reveals DPI settings, while hover and pointer queries identify input method preferences:

@media (hover: hover) and (pointer: fine) {
  /* Desktop with mouse input */
}

@media (hover: none) and (pointer: coarse) {
  /* Touch-only mobile device */
}

These characteristics combine to narrow user identity significantly. A user with specific screen resolution, color depth, and input preferences becomes identifiable across sessions.

The <link rel="preload"> directive enables tracking through conditional resource loading. Stylesheets can specify preload links that only resolve when certain conditions match:

<link rel="preload" href="/track?feature=wide-screen" as="style"
      media="(min-width: 1920px)">
<link rel="preload" href="/track?feature=mobile" as="style"
      media="(max-width: 767px)">

When the browser evaluates these conditions and initiates the preload request, the server logs the request with associated metadata—IP address, User-Agent, and timing information. This creates a server-side log of device characteristics without any client-side script execution.

CSS Attribute Selectors for State Detection

Attribute selectors with the presence indicator ([attr]) or exact matching ([attr="value"]) enable detecting element states and browser behaviors. While not directly exposing information to servers, these techniques work alongside other mechanisms:

input[type="password"] {
  background-image: url('/tracker?input-type=password-detected');
}

a[href^="tel:"] {
  background-image: url('/tracker?feature=tel-link-supported');
}

a[href^="mailto:"] {
  background-image: url('/tracker?feature=mailto-supported');
}

When the browser renders elements matching these selectors, it attempts to load the specified background image, generating a server-side request that reveals capability information.

Counter-Based Session Tracking

CSS counters provide a subtle tracking mechanism by maintaining state across page views within a single session. While limited in persistence, they demonstrate CSS’s stateful capabilities:

body {
  counter-reset: session 0;
}

a::after {
  content: counter(session);
  display: none;
}

More sophisticated implementations use dynamic style injection through CSS variables, though this typically requires minimal JavaScript coordination. The core concept remains: CSS maintains internal state that applications can observe.

Privacy Implications and Defenses

CSS fingerprinting operates below the threshold of common privacy tools. Unlike JavaScript-based tracking, it bypasses NoScript, uMatrix, and other script-blocking extensions. Users relying solely on script blocking remain vulnerable to CSS-based tracking vectors.

Defensive strategies include restricting font visibility through system configuration, using browser features that normalize reported screen properties, and employing content blockers that filter tracking URLs. Developers can mitigate fingerprinting by avoiding font-based user identification, normalizing CSS-reported values, and implementing Content Security Policy headers that block tracking resource loading.

The broader privacy community continues developing tools like privacy.resistFingerprinting in Firefox and similar protections in other browsers. These settings attempt to standardize reported values, making individual users indistinguishably similar to the broader population.


CSS fingerprinting demonstrates that web tracking persists through mechanisms beyond JavaScript. Stylesheets provide sufficient expressiveness for device identification, font profiling, and capability detection. For developers prioritizing user privacy, understanding these techniques informs both defensive implementation and respectful data practices. The web benefits when we treat every vector—including CSS—as worthy of privacy consideration.

Measuring CSS Fingerprint Entropy

Like JavaScript fingerprinting, CSS fingerprints can be quantified by their entropy:

import math

def calculate_css_fingerprint_entropy():
    """Estimate entropy of CSS fingerprinting vectors."""

    vectors = {
        "screen_width": 300,  # Possible values: 320-5120 in 10px increments
        "screen_height": 200,
        "color_depth": 3,  # 8-bit, 24-bit, 32-bit
        "installed_fonts": 50,  # 10-500 fonts typically
        "pointer_type": 4,  # none, coarse, fine, unknown
        "hover_capable": 2,  # true/false
        "prefers_dark_mode": 2,  # true/false
        "prefers_reduced_motion": 2
    }

    total_entropy = 0
    for vector, possible_values in vectors.items():
        entropy = math.log2(possible_values)
        total_entropy += entropy
        print(f"{vector}: {entropy:.2f} bits ({possible_values} values)")

    print(f"\nTotal CSS fingerprint entropy: {total_entropy:.1f} bits")
    print(f"Can uniquely identify: 2^{total_entropy:.1f} = {2**total_entropy:.0e} users")

calculate_css_fingerprint_entropy()

Output:

This demonstrates that CSS fingerprinting alone can identify users within populations of millions.

Practical CSS Fingerprinting Attack: Step-by-Step

Here’s how a real tracking network might deploy CSS fingerprinting:

<!-- Malicious website serving tracking CSS -->
<!DOCTYPE html>
<html>
<head>
<style>
  /* Detect screen size through media queries */
  @media (min-width: 1920px) {
    body::after { content: attr(data-width); display: none; }
  }

  /* Detect font availability -->
  @font-face {
    font-family: 'detect-arial';
    src: url('https://tracker.com/fp?font=Arial') format('woff2');
  }

  @font-face {
    font-family: 'detect-georgia';
    src: url('https://tracker.com/fp?font=Georgia') format('woff2');
  }

  body {
    font-family: detect-georgia, georgia, serif;
  }

  /* Detect color depth -->
  @media (color-gamut: srgb) {
    body::before { content: 'srgb'; }
  }

  /* Detect input capability -->
  @media (hover: hover) {
    .has-hover { display: block; }
  }
</style>
</head>
<body>
<p class="has-hover">Hover-capable device</p>

<!-- Collect all fingerprint signals -->
<script>
  // Browser will load fonts if available, sending requests to tracker
  // Media query results visible to JavaScript
  // Combined fingerprint sent to tracking server
</script>
</body>
</html>

The tracker’s server logs:

  1. Which fonts were loaded (available on device)
  2. Screen resolution from media query evaluation
  3. Input capabilities
  4. Browser behavior patterns

Defensive CSS Implementation

As a web developer, build websites that respect privacy:

/* Privacy-conscious font strategy */

/* 1. Avoid @font-face trackers - use system fonts */
body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  /* Provides native look without external font requests */
}

/* 2. Never use fonts for fingerprinting */
/* WRONG: */
@font-face {
  font-family: 'tracking-font';
  src: url('/track?font=present');
}

/* RIGHT: */
@font-face {
  font-family: 'local-fallback';
  src: local('Helvetica'), local('Arial'), local(sans-serif);
}

/* 3. Implement Content Security Policy to prevent tracking stylesheets */
/* Set in HTTP headers or meta tag */
<meta http-equiv="Content-Security-Policy"
      content="style-src 'self'; script-src 'self';">

/* 4. Avoid data exfiltration through CSS */
/* WRONG: */
input[type="password"] {
  background: url('/tracker?input-type=password');
}

/* RIGHT: */
input[type="password"] {
  /* No external resources */
  border: 1px solid #ccc;
}

/* 5. Normalize media query exposure */
/* Use relative units instead of fixed pixels */
@media (min-width: 45em) { /* 45em ≈ 720px - hides exact size */
  .sidebar { display: block; }
}

/* 6. Don't fingerprint through counter states */
/* This leaks session information */
body { counter-reset: analytics; }
/* ^ Don't do this */

Browser Extensions for CSS Fingerprinting Defense

uMatrix ($0, free, Chrome/Firefox): Blocks all external stylesheet loads by default. Whitelist only trusted sites. Prevents @font-face tracking.

NoScript ($0, free, Firefox): Blocks JavaScript but allows CSS. Pair with CSP headers for defense-in-depth.

Privacy Badger ($0, free, Chrome/Firefox): Learns to block trackers based on behavior. Includes CSS tracker detection.

uBlock Origin ($0, free, Chrome/Firefox): Advanced content blocker that can target tracking URLs in stylesheets.

CSS Guard ($0, free, Chrome): Specifically designed for CSS fingerprinting defense. Randomizes CSS-detectable properties.

Server-Side Mitigations

Web developers can reduce fingerprinting attack surface through server configuration:

# Apache configuration to prevent fingerprinting

# 1. Disable directory listing (reduces information leakage)
<Directory /var/www/html>
    Options -Indexes
</Directory>

# 2. Set CSP headers to prevent tracking stylesheet injection
Header set Content-Security-Policy "style-src 'self' data:; object-src 'none'; img-src 'self'; font-src 'self'"

# 3. Disable X-powered-by headers (don't reveal technology stack)
Header always unset X-Powered-By

# 4. Set Permissions-Policy to limit capability detection
Header set Permissions-Policy "geolocation=(), microphone=(), camera=()"

# 5. Prevent MIME type sniffing (reduces inference attacks)
Header set X-Content-Type-Options: nosniff
# Python/Flask implementation
from flask import Flask
from flask_talisman import Talisman

app = Flask(__name__)

# Apply security headers
Talisman(app,
    content_security_policy={
        'default-src': "'self'",
        'style-src': ["'self'", "'unsafe-inline'"],  # CSS needs inline for performance
        'font-src': "'self'",
        'img-src': "'self'",
        'script-src': "'self'"
    },
    force_https=True,
    strict_transport_security=True,
    content_security_policy_nonce_in=['script-src']
)

# Disable fingerprinting headers
@app.after_request
def remove_fingerprinting_headers(response):
    response.headers.pop('X-Powered-By', None)
    response.headers.pop('Server', None)
    return response

Testing Your CSS Fingerprinting Defenses

Validate that your website isn’t helping fingerprinting:

#!/bin/bash
# CSS fingerprinting vulnerability scanner

TARGET_URL="https://example.com"

echo "Scanning $TARGET_URL for CSS fingerprinting vectors..."

# 1. Check for external font requests
echo "Checking for external fonts..."
curl -s "$TARGET_URL" | grep -i "@font-face" | grep -v "local"

# 2. Verify CSP headers
echo "Checking Content-Security-Policy..."
curl -sI "$TARGET_URL" | grep -i "content-security"

# 3. Look for media query tracking
echo "Checking for media query trackers..."
curl -s "$TARGET_URL" | grep -E "@media.*url"

# 4. Verify no CSS-based data exfiltration
echo "Checking for background-image tracking..."
curl -s "$TARGET_URL" | grep -E "background.*url.*tracker"

echo "Scan complete"

Built by theluckystrike — More at zovo.one