React Security Best Practices
In this page:
React's Built-In XSS Protection
When you render a value inside JSX like {userInput}, React automatically escapes it before inserting it into the DOM, meaning even if userInput contains HTML/script tags, it's displayed as plain text rather than executed as code. This default behavior protects against a large class of cross-site scripting (XSS) attacks automatically.
Note: This automatic escaping applies to normal JSX interpolation — you get this protection 'for free' just by using React the normal way.
Warning: This protection ONLY applies to normal JSX rendering — it does not extend to dangerouslySetInnerHTML, which explicitly opts out of it.
Example: React's Built-In XSS Protection
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
function App() {
const userInput = "<img src=x onerror=alert('hacked')>";
return <p>{userInput}</p>; // Rendered as literal text, NOT executed as HTML
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>
</body>
</html>
The Danger of dangerouslySetInnerHTML
dangerouslySetInnerHTML lets you inject raw HTML directly into the DOM, bypassing React's automatic escaping entirely. If that HTML comes from user input (or any untrusted source) without being sanitized first, it opens a direct path for an attacker to run arbitrary JavaScript on your page.
Note: If you must render user-provided HTML (like a rich-text comment), sanitize it first with a dedicated library like DOMPurify before passing it to dangerouslySetInnerHTML.
Warning: The name isn't dramatic for no reason — using this with unsanitized, untrusted content is a genuine, exploitable XSS vulnerability, not a theoretical risk.
Example: The Danger of dangerouslySetInnerHTML
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
function App() {
const trustedContent = "<strong>This is safe because WE wrote it, not a user</strong>";
return <div dangerouslySetInnerHTML={{ __html: trustedContent }} />;
// Never pass raw, unsanitized USER input here without a sanitizer like DOMPurify first
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>
</body>
</html>
Client-Side Validation Isn't Real Security
Validating a form field in the browser (like checking an email format before submit) improves user experience by giving instant feedback, but a malicious user can bypass all client-side JavaScript entirely and send whatever data they want directly to your server. Real security and validation must always happen server-side too.
Note: Think of client-side validation purely as a UX nicety — the server must independently re-validate and authorize every request as if the client-side checks never happened.
Warning: Relying only on client-side checks for anything security-sensitive (permissions, data validation, pricing) is trivially bypassable by anyone using browser devtools or a direct API request.
Example: Client-Side Validation Isn't Real Security
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
function App() {
const [email, setEmail] = React.useState("");
const isValidLooking = email.includes("@"); // UX nicety only
return (
<div>
<input value={email} onChange={e => setEmail(e.target.value)} />
<p>{isValidLooking ? "Looks valid (still re-check on the server!)" : "Enter an email"}</p>
</div>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>
</body>
</html>
- Using dangerouslySetInnerHTML with unsanitized user-provided content, opening the door to cross-site scripting (XSS).
- Storing sensitive tokens (like auth tokens) in localStorage, which is readable by any JavaScript running on the page, including injected malicious scripts.
- Trusting client-side-only validation as if it were real security, when a malicious user can bypass client-side JavaScript entirely.
- React automatically escapes values rendered in JSX, protecting against XSS in most normal usage.
- dangerouslySetInnerHTML bypasses this protection and should only be used with content you've explicitly sanitized.
- Sensitive data (tokens, secrets) shouldn't be stored in localStorage, which any page script can read.
- Client-side validation is a UX convenience, never a substitute for real server-side validation and authorization.
No specific React version requirement — these are general web security practices applied in a React context.
Chapter Quiz — Complete all 14 topics to unlock
0/14 topics done
Complete these topics first:
- Error Boundaries
- React Portals
- Modals using Portals (practical use)
- React Suspense
- Code Splitting with React.lazy
- Introduction to Server Components
- Introduction to Next.js (server-side React)
- Using React with TypeScript
- Scalable Folder Architecture
- Common React Design Patterns
- Component Documentation with Storybook
- Accessibility (a11y) in React
- i18n with react-i18next
- React Security Best Practices