Back to Blog
Web Security

XSS in React 2025: Modern Attacks and Defenses

AliceSec Team
2 min read

React's automatic escaping makes XSS harder—but not impossible. Signal had to patch a React-based XSS vulnerability related to improper HTML handling. A 2024 security analysis revealed that XSS vulnerabilities continue to affect React applications, often with more severe consequences than traditional apps.

This guide covers the specific ways XSS bypasses React's protections and how to defend against them in 2025.

How React Protects Against XSS

React automatically escapes values embedded in JSX:

jsx
// Safe - React escapes the script tags
const userInput = "<script>alert('xss')</script>";
return <div>{userInput}</div>;
// Renders: &lt;script&gt;alert('xss')&lt;/script&gt;

This default protection stops most XSS attacks. But several APIs and patterns bypass it entirely.

Attack Vector 1: dangerouslySetInnerHTML

The most common React XSS vulnerability:

jsx
// VULNERABLE - Raw HTML injection
function BlogPost({ content }) {
  return <div dangerouslySetInnerHTML={{ __html: content }} />;
}

// Attack payload in content:
// "<img src=x onerror=alert(document.cookie)>"

Defense: DOMPurify

Always sanitize before using dangerouslySetInnerHTML:

jsx
import DOMPurify from 'dompurify';

function SafeBlogPost({ content }) {
  const sanitizedContent = DOMPurify.sanitize(content, {
    ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'ul', 'ol', 'li', 'a'],
    ALLOWED_ATTR: ['href', 'target', 'rel'],
  });

  return <div dangerouslySetInnerHTML={{ __html: sanitizedContent }} />;
}

Better: Create a SafeHTML Component

Encapsulate sanitization in a reusable component:

jsx
import DOMPurify from 'dompurify';

interface SafeHTMLProps {
  html: string;
  allowedTags?: string[];
  className?: string;
}

export function SafeHTML({ html, allowedTags, className }: SafeHTMLProps) {
  const clean = DOMPurify.sanitize(html, {
    ALLOWED_TAGS: allowedTags || ['p', 'br', 'strong', 'em', 'a'],
    ALLOWED_ATTR: ['href', 'class'],
    ADD_ATTR: ['target'],
  });

  return (
    <div
      className={className}
      dangerouslySetInnerHTML={{ __html: clean }}
    />
  );
}

// Usage
<SafeHTML html={userContent} allowedTags={['p', 'br']} />

Attack Vector 2: URL Injection (href/src)

React doesn't sanitize URL attributes:

jsx
// VULNERABLE - javascript: URLs execute code
function UserLink({ url, label }) {
  return <a href={url}>{label}</a>;
}

// Attack: url = "javascript:alert(document.cookie)"

Defense: URL Validation

jsx
function isValidURL(url: string): boolean {
  try {
    const parsed = new URL(url);
    // Only allow safe protocols
    return ['http:', 'https:', 'mailto:'].includes(parsed.protocol);
  } catch {
    return false;
  }
}

function SafeLink({ url, label }: { url: string; label: string }) {
  if (!isValidURL(url)) {
    return <span>{label}</span>; // Render as plain text
  }

  return (
    <a href={url} rel="noopener noreferrer">
      {label}
    </a>
  );
}

Attack Vector 3: ref-based DOM Manipulation

Direct DOM access bypasses React's protection:

jsx
// VULNERABLE - Direct innerHTML assignment
function DangerousComponent({ userContent }) {
  const divRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (divRef.current) {
      divRef.current.innerHTML = userContent; // XSS!
    }
  }, [userContent]);

  return <div ref={divRef} />;
}

Defense: Avoid Direct DOM Manipulation

jsx
// SAFE - Let React handle rendering
function SafeComponent({ userContent }) {
  const sanitized = DOMPurify.sanitize(userContent);
  return <div dangerouslySetInnerHTML={{ __html: sanitized }} />;
}

Attack Vector 4: Server-Side Rendering (SSR)

SSR can expose XSS if state isn't properly escaped:

jsx
// VULNERABLE - Server-rendered state injection
function ServerPage({ initialData }) {
  return (
    <html>
      <body>
        <div id="root" />
        <script
          dangerouslySetInnerHTML={{
            __html: `window.__INITIAL_STATE__ = ${JSON.stringify(initialData)}`
          }}
        />
      </body>
    </html>
  );
}

// Attack: initialData contains </script><script>alert(1)</script>

Defense: Serialize-JavaScript

jsx
import serialize from 'serialize-javascript';

function SafeServerPage({ initialData }) {
  return (
    <html>
      <body>
        <div id="root" />
        <script
          dangerouslySetInnerHTML={{
            __html: `window.__INITIAL_STATE__ = ${serialize(initialData, { isJSON: true })}`
          }}
        />
      </body>
    </html>
  );
}

Attack Vector 5: Third-Party Components

External libraries may not follow security best practices:

jsx
// POTENTIALLY VULNERABLE
// Some rich text editors render HTML without sanitization
import { RichTextEditor } from 'some-library';

function Editor({ content }) {
  return <RichTextEditor value={content} />;
}

Defense: Audit and Wrap

jsx
// Wrap third-party components with sanitization
function SafeRichTextDisplay({ content }) {
  const sanitized = DOMPurify.sanitize(content);
  return <RichTextEditor value={sanitized} readOnly />;
}

Defense Layer: Content Security Policy

CSP provides defense-in-depth:

jsx
// next.config.js (Next.js example)
const securityHeaders = [
  {
    key: 'Content-Security-Policy',
    value: `
      default-src 'self';
      script-src 'self' 'nonce-{RANDOM}';
      style-src 'self' 'unsafe-inline';
      img-src 'self' data: https:;
      font-src 'self';
      object-src 'none';
      base-uri 'self';
      form-action 'self';
      frame-ancestors 'none';
    `.replace(/\n/g, '')
  }
];

Defense Layer: ESLint Rules

Detect dangerous patterns automatically:

json
{
  "plugins": ["react", "jam3"],
  "rules": {
    "react/no-danger": "warn",
    "jam3/no-sanitizer-with-danger": "error",
    "react/jsx-no-script-url": "error",
    "react/jsx-no-target-blank": "error"
  }
}

React XSS Prevention Checklist

dangerouslySetInnerHTML

  • [ ] Always sanitize with DOMPurify before use
  • [ ] Create wrapper component for consistent sanitization
  • [ ] Configure allowed tags/attributes appropriately
  • [ ] Use ESLint rules to flag unsanitized usage

URLs

  • [ ] Validate all user-provided URLs
  • [ ] Block javascript: and data: protocols
  • [ ] Use allowlist of permitted protocols
  • [ ] Add rel="noopener noreferrer" to external links

SSR

  • [ ] Use serialize-javascript for state serialization
  • [ ] Escape HTML entities in server-rendered content
  • [ ] Implement CSP headers

General

  • [ ] Enable strict CSP
  • [ ] Audit third-party components
  • [ ] Keep dependencies updated
  • [ ] Run security scanning in CI/CD

Practice XSS Attacks

Understanding how XSS works is essential for building secure React applications. Practice identifying and exploiting XSS vulnerabilities in our XSS challenges.

---

React security evolves with each release. This guide will be updated as new attack vectors and defenses emerge. Last updated: December 2025.

Stay ahead of vulnerabilities

Weekly security insights, new challenges, and practical tips. No spam.

Unsubscribe anytime. No spam, ever.