CSS Injections (CSSI): when a simple color opens the door to XSS 🎨
CSS Injections (CSSI) are a type of web vulnerability that allows an attacker to inject malicious CSS code into a page, taking advantage of the fact that the application takes user input and dumps it directly into its stylesheet without validating or sanitizing it. At first glance it may seem like a harmless flaw (“it’s just CSS, it can’t execute code”), but as we’ll see, it can escalate into becoming a full-blown Cross-Site Scripting (XSS) vector.
🔍 Scenario
To practice this we use the skf-labs/nodejs/CSSI lab. First, we install the project dependencies:
npm install
The lab has a form that lets you enter a color. For example, if we type red, the application dynamically generates the following style tag:
<style>
p.colorful {
color: red
}
</style>
The problem becomes evident as soon as we look at how that block is built: the value we enter is inserted as-is inside the CSS, without any kind of filtering.
💡 Step 1 — Injecting an extra CSS property
If we put the following into the form field:
red;background-color:green
We’re injecting additional CSS within the same rule, altering the element’s style beyond what the developer expected. It’s a first sign that there’s no control over the input, but the real impact goes much further than this example.
💡 Step 2 — Escaping the CSS rule
If we close the brace of the current rule, we can define styles that affect any other selector on the page, for example body:
red}body{ background-color:yellow
Here we’re no longer just modifying the original element: we’re escaping the context intended by the developer and taking control of the entire stylesheet.
💡 Step 3 — From CSS to JavaScript
The final blow comes when, in addition to closing the CSS rule, we also close the <style> tag itself and take the opportunity to inject a <script> tag:
red}</style><script>alert("HACKED")</script>
At this point we’re no longer talking about a simple style injection: we’ve managed to execute arbitrary JavaScript code in the context of the page, that is, a classic XSS disguised behind a field that was supposedly only accepting a color.
🚫 What went wrong?
The underlying flaw is always the same: the application trusts untrusted user input and embeds it directly into the page’s CSS (or HTML) code, without validating or escaping special characters such as ;, }, or <. This turns a seemingly innocuous field into an entry point for manipulating the page’s design or, in the worst case, for executing JavaScript.
CSS Injections can be used on their own as an attack vector to exploit Cross-Site Scripting (XSS) vulnerabilities. Imagine a web application that lets users enter text in a field that is later displayed on the page: if the developer doesn’t properly validate or filter that input, an attacker could inject malicious code, including JavaScript, just as we saw in step 3.
Additionally, if the injected CSS code is “complex enough,” it can cause the browser to interpret the code in a way that ends up triggering JavaScript execution, which is known as CSS-Induced JavaScript Injection. Once the attacker achieves that entry point, they can deploy a full XSS attack: stealing credentials, hijacking sessions, or redirecting the victim to a fake page, among many other scenarios. To dive deeper into CSSI exploitation techniques, HackTricks maintains a detailed collection on their CSS Injection page.
✅ Best practices
- Never insert user input directly into
<style>blocks orstyleattributes without sanitizing it. - Validate expected values against a whitelist (for example, if a color is expected, check that it matches a valid color pattern).
- Properly escape special CSS/HTML characters (
;,{,},<,>) before inserting them into the DOM. - Apply a Content Security Policy (CSP) that restricts inline script execution.
🔐 Conclusion
CSS Injections show that no user input point is “too small” to be dangerous. A simple, poorly validated color field can end up leading to arbitrary JavaScript execution and a full Cross-Site Scripting attack. The lesson, as always: all user input is hostile until proven otherwise.
