March 20, 2018

Tracking Users with CSS

In early 2018, a physics student named Jan Böhmer created a website that tracks and records user data including clicks, mouse movements, browser type, and OS. While user tracking is nothing new, his approach doesn’t require JavaScript, plugins, or external libraries. In fact, it uses little more than plain HTML and a bit of CSS.

How It Works

Böhmer’s concept leverages two features of CSS: the ability to inject content into HTML elements, and the ability to change the style after a user performs an action. The website works by using the content property to set a URL when an action is performed. The URL calls a script that records details about the actions, which are passed as URL parameters. Setting this URL with the ::before and ::after CSS selectors ensures that the URL is only called when an action is performed, rather than when the page first loads.

For example, the following CSS calls the URL each time the #link element is clicked:

#link:active::after {
    content: url("");

The track script contains code that records the time of the event and the action performed. It can also be used to extract the user’s IP address, user agent, and other identifying information.

Here is an example of how such a script could look in PHP:


// Prints the time that the script ran
print("Timestamp: " . time());

// Prints the action specified by the action parameter (in this case, "link_clicked")
print("Action: " . $_REQUEST['action']);

// Prints the user's IP address
print("IP Address: " . $_SERVER['REMOTE_ADDR']);

// Prints the user's browser agent
print("User Agent: " . $_SERVER['HTTP_USER_AGENT']);

Detecting the Browser Type

Users can spoof their browser’s user agent, but Böhmer skirts around this by testing for browser-specific CSS properties using the @supports at-rule. For example, the following action detects Chrome browsers by detecting that -webkit-appearance is available, and that -ms-ime-align is not available:

@supports (-webkit-appearance:none) and (not (-ms-ime-align:auto)){
    #chrome_detect::after {
        content: url("");

Detecting the Operating System

Böhmer even uses font detection to try to identify the user’s operating system. For example, by detecting whether the browser supports the Calibri font family, we can assume that the browser is running in Windows:

// stylesheet.css
@font-face {
    font-family: Font1;
    src: url("");

#font_detection {
    font-family: Calibri, Font1;
<!-- page.html -->
<div id="font_detection">test</div>

Böhmer’s proof of concept can identify other data points including the browser window size and orientation, whether the user has clicked a link, and how much time the user has spent hovering over an element.

This attack is extremely difficult to prevent within the browser. The only way to prevent it completely is to disable CSS, which can make websites unusable. However, it is possible to reduce the chances of an attacker exploiting this vulnerability through the use of a Content Security Policy (CSP).

Mitigating CSS Leaks Using Content Security Policies

A CSP is a set of rules that determines what actions a browser can and can’t perform. CSPs are commonly used to prevent cross-site scripting (XSS) and other attacks caused by browsers loading untrusted scripts. While normally used with JavaScript files, CSPs can also apply to CSS styles and stylesheets.

Consider a website that uses a stylesheet hosted by a third-party provider. An attacker compromises the stylesheet and adds user tracking to a link on the page:

// Malicious CSS
#link:active::after {
    content: url("");
<!-- page.html -->
<a href="" id="link">Click here</a>

When a user clicks the link, their browser calls the tracking script hosted on Since this is done entirely through the browser, the website owner is completely unaware of the exploit. A Content-Security-Policy prevents this by setting rules on which styles are allowed and where they can originate from.

Disabling Inline Styles

Disabling inline styles is one of the biggest security benefits provided by CSP. Inline styles are styles declared directly in the HTML document (or set using JavaScript), rather than those loaded from a stylesheet. Inline styles - especially dynamically generated styles or user created styles - are extremely difficult to secure. This is why CSP commonly block all inline scripts and styles and whitelists those that have been specifically approved.

The following rule blocks all inline styles, as well as externally hosted stylesheets:

Content-Security-Policy "style-src 'self';"

Verifying Styles Using Hashes and Nonces

If blocking inline styles isn’t feasible, you can still ensure the integrity of your CSS using hashes and nonces.

A hash is a one-way string generated from the contents of a file or string. When a hashing function is performed on a stylesheet or inline style, it always returns the same results unless the style changes. This is useful for whitelisting certain inline styles and stylesheets, while verifying that the style hasn’t been modified or tampered with.

Content-Security-Policy "default-src 'self'; style-src 'sha256-MUNBNkRCNDMwQzZFNjI4Mzc2MzIzNTMwOEFCMEZEMzkxQjVGN0NGMEE5MjBFRDQ2N0MwNTgzNkUwRDIzNTdCRA=='"
// validstylesheet.css
#link {
    color: blue;
    Font-style: bold;

Nonces perform a similar function as hashes. With a nonce, a new random number is generated for each request, making it more difficult for attackers to guess its value. This avoids a key shortcoming of hashes, which is that it’s possible for more than one input to generate the same hash (known as a collision).

Content-Security-Policy "default-src 'self'; style-src 'nonce-SGVsbG8gd29ybGQh"
// page.html
<style nonce="SGVsbG8gd29ybGQh"></style>

Verifying Stylesheets Hosted Externally

Stylesheets are often hosted on third-party servers such as content delivery networks (CDNs), but this opens up a new attack vector. If the CDN is compromised, what’s to stop an attacker from replacing a stylesheet with their own modified version? Subresource integrity, or SRI, attempts to resolve this.

SRI uses hashes to verify the contents of scripts and stylesheets. Each file’s hash is calculated and appended to the HTML element’s integrity attribute. When the browser downloads the script or stylesheet, it calculates its hash and compares it to the value stored in the attribute. If it’s a match, the browser loads the script or style.

// page.html
<style src="" integrity="sha256-wFNeS+K3n/2TKRMFQ2v4iTFOSj+uwF7P/Lt98xrZ5Ro=" crossorigin="anonymous"></style>
// style.css
#link {
    color: blue;
    Font-style: bold;

This works under the assumption that the web page is delivered from a trusted source (such as an origin server), while the resource is delivered from an untrusted source (such as a third party). If both the web page and resources are hosted by a third party, an attacker could simply modify the web page to match the hash of their replacement CSS file.


While the ability to track users via CSS is nothing new, it does require us to think differently about privacy and security on the web. CSS is one of the fundamental languages of the modern web, and disabling CSS for websites would make much of the web unusable. Content-Security-Policy is the best way to protect from XSS attacks and CSS leaks. Templarbit created an “Agile Content-Security-Policy Workflow” to make it easy to maintain CSP headers. If your team is struggling with rolling out CSP for your applications, start with a free account and learn more about Templarbit’s approach on how to solve CSS leaks.

You can find the source code to Böhmer’s proof of concept on GitHub.