React gives teams a safer rendering model than manual DOM manipulation, but it does not make frontend security automatic. Most serious React security issues appear where the framework stops helping: authentication storage, embedded HTML, browser security headers, third-party packages, and build-time dependency trust.
The practical question is not “Is React secure?” A better question is: where can untrusted data cross a boundary, and what prevents it from becoming code, credentials, or production access? That framing leads to better decisions than a generic checklist.
What React Protects, and What It Does Not
React escapes interpolated values by default. This is useful because values rendered as text do not become executable HTML.
function ProfileName({ name }: { name: string }) {
return <h2>{name}</h2>;
}If name contains <script>alert(1)</script>, React renders it as text, not as a script. That protection is real, but narrow.
React does not protect you from:
Unsafe use of
dangerouslySetInnerHTMLDirect DOM writes outside React
Token exposure through JavaScript-accessible storage
Weak Content Security Policy
Malicious or compromised npm packages
Unsafe redirects, URL handling, or iframe embedding
Backend APIs that trust frontend state
React reduces accidental DOM-based XSS, but it cannot compensate for unsafe trust boundaries.
The strongest React security posture starts by treating the frontend as hostile to itself. Any code running in the browser can be influenced by browser extensions, injected scripts, compromised dependencies, or earlier XSS bugs.
Auth Storage: localStorage Is Convenient, Not Neutral
A common React authentication shortcut is to store access tokens in localStorage and attach them to API requests.
// Common shortcut, risky under XSS
const token = localStorage.getItem("access_token");
await fetch("/api/account", {
headers: {
Authorization: `Bearer ${token}`,
},
});This is simple and works across reloads, but it changes the impact of XSS. If an attacker can execute JavaScript, they can read the token and move it outside the application. The XSS bug becomes an account takeover path.
The storage choice should be made as an architecture decision, not as a frontend convenience.
Storage model | JavaScript readable | Survives reload | XSS token theft risk | CSRF exposure | Operational complexity |
|---|---|---|---|---|---|
localStorage | ✓ | ✓ | High | Low for bearer headers | Low |
sessionStorage | ✓ | Per tab | High | Low for bearer headers | Low |
In-memory variable | ✓ while page lives | ✗ | Medium, harder to persist | Low for bearer headers | Medium |
HttpOnly secure cookie | ✗ | ✓ | Lower for direct token theft | Needs CSRF controls | Medium |
Backend-for-frontend session | ✗ | ✓ | Lower in browser | Needs CSRF controls | Higher |
HttpOnly cookies are not automatically “more secure” in every design. They reduce direct token theft from JavaScript, but cookie-based authentication needs CSRF protection, correct SameSite behavior, secure transport, and careful domain scoping.
A production-grade React app usually benefits from one of these patterns:
Short-lived access token in memory, refresh handled through a protected HttpOnly cookie
Server-side session with HttpOnly cookie and explicit CSRF defense
Backend-for-frontend layer that keeps sensitive tokens away from browser JavaScript
Frontend code then uses credentialed requests instead of manually handling long-lived bearer tokens.
// Frontend does not read the refresh token
await fetch("/api/account", {
method: "GET",
credentials: "include",
});This does not eliminate XSS, but it reduces what XSS can steal directly. That difference matters in incident response.
dangerouslySetInnerHTML: Make It Rare, Visible, and Sanitized
dangerouslySetInnerHTML is intentionally named. It bypasses React’s normal escaping and tells the browser to interpret a string as HTML.
// Unsafe if html comes from users, CMS content, markdown, support tickets, or external APIs
function ArticlePreview({ html }: { html: string }) {
return <div dangerouslySetInnerHTML={{ __html: html }} />;
}This should not appear casually across a codebase. In a mature React application, raw HTML rendering should be centralized behind a small component that documents the trust boundary.
import DOMPurify from "dompurify";
type SafeHtmlProps = {
html: string;
};
export function SafeHtml({ html }: SafeHtmlProps) {
const sanitized = DOMPurify.sanitize(html, {
USE_PROFILES: { html: true },
});
return <div dangerouslySetInnerHTML={{ __html: sanitized }} />;
}This is still not a free pass. Sanitization must be configured for the allowed content model. Rendering product descriptions, markdown comments, rich CMS pages, and email templates can require different policies.
A useful internal rule is simple: application code should not call dangerouslySetInnerHTML directly. It should call a vetted component with tests around allowed and blocked input.
// Preferred application usage
<SafeHtml html={article.bodyHtml} />The review question should be: “Why is HTML necessary here?” If plain text, markdown-to-React components, or structured data can solve the same problem, avoid raw HTML entirely.
CSP Turns XSS From One Bug Into a Contained Failure
Content Security Policy is not a substitute for escaping or sanitization. Its value is containment. A good CSP can make common XSS payloads harder to execute, prevent unexpected script origins, and reduce the ability to exfiltrate data.
A strict CSP for React applications is usually easier when the app avoids inline scripts and inline event handlers. Build pipelines, analytics snippets, tag managers, and legacy widgets often make this harder than the React code itself.
A practical starting point might look like this:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{RANDOM_NONCE}';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
connect-src 'self' https://api.example.com;
frame-ancestors 'none';
base-uri 'self';
object-src 'none';The exact policy depends on hosting, analytics, asset delivery, API domains, and whether the app uses server-side rendering. The important part is not copying a header. It is reducing permissions until required behavior breaks, then adding back only what is justified.
Pay special attention to:
script-src, because it controls executable script sourcesconnect-src, because it controls where frontend code can send dataframe-ancestors, because it controls clickjacking exposureobject-src 'none', because plugin content is rarely neededbase-uri 'self', because base tag injection can alter URL resolution
Avoid treating CSP as a deployment-only concern. It belongs in application architecture because product decisions often require new script origins, frames, image hosts, or API endpoints.
npm Supply-Chain Risk Is a Frontend Security Issue
React security is not limited to code written by your team. Modern frontend builds execute a large amount of third-party JavaScript during installation, bundling, testing, and runtime. A compromised package can affect the build pipeline before it ever reaches the browser.
The risk is not only “a dependency has a vulnerability.” Supply-chain exposure includes:
Malicious install scripts
Dependency confusion
Typosquatted packages
Compromised maintainer accounts
Unmaintained transitive dependencies
Excessive package permissions in CI
Bundled code that changes without review
A stronger workflow is usually more useful than sporadic manual audits.
npm ci
npm audit --audit-level=high
npm outdatedThose commands do not solve supply-chain security by themselves, but they make dependency state visible. For production teams, visibility should be combined with process:
Use lockfiles and reproducible installs with
npm ciReview new direct dependencies before adding them
Avoid packages for trivial utilities
Pin CI permissions to the minimum required
Run dependency checks in pull requests
Monitor runtime bundle contents, not just
package.jsonPrefer maintained packages with clear ownership and release history
The most important habit is to treat dependency additions as architecture changes. Every new package increases code volume, update responsibility, and possible compromise paths.
A Frontend Security Checklist for React Teams
A checklist is only useful when it maps to code review, CI, and deployment. This one focuses on decisions that can be enforced or reviewed.
Rendering and XSS
User-controlled values are rendered through React interpolation, not string-built HTML.
dangerouslySetInnerHTMLis limited to a single reviewed component.HTML sanitization is tested with malicious examples.
Direct DOM writes are avoided or isolated.
User-generated URLs are validated before use in links, redirects, or images.
Authentication and Session Handling
Long-lived tokens are not stored in
localStorage.Refresh tokens are not readable from browser JavaScript.
Cookie-based flows include CSRF controls.
Access tokens are short-lived when bearer tokens are required.
Logout clears both frontend state and server-side session state where applicable.
Browser Policy and Headers
CSP is deployed and monitored.
Inline scripts are avoided where possible.
frame-ancestorsis set deliberately.Production uses HTTPS-only cookies with appropriate
SameSiteandSecureattributes.Error pages and fallback routes do not expose sensitive data.
Dependency and Build Pipeline
Lockfiles are committed and used in CI.
Dependency updates are reviewed, not blindly merged.
New packages are justified during code review.
CI tokens have limited scope.
Build artifacts are produced from a clean, repeatable install.
What to Adopt First
Teams often fail by trying to fix everything at once. A better sequence is to reduce high-impact risks first.
Remove persistent tokens from
localStoragewhere practical.Centralize and test all raw HTML rendering.
Add a CSP that starts in report-only mode, then enforce it after tuning.
Lock down dependency installation in CI.
Add security checks to pull requests instead of relying on occasional audits.
This order works because it targets blast radius. Token theft, raw HTML execution, and compromised dependencies are the places where a small bug can become a large incident.
For engineers who work with React in production and want a structured way to validate practical senior-level skills, the Senior React Developer certification is the most relevant DevCerts track to review.
Conclusion
React security is not a single setting or library. It is a set of boundaries: what can become HTML, what can become executable JavaScript, what can read credentials, what can send data out, and what code enters the build.
The mature approach is not to trust React less. It is to understand exactly where React helps and where the application must provide its own controls. Store credentials with incident impact in mind, make raw HTML exceptional, use CSP as containment, and treat npm dependencies as part of the attack surface. That is the difference between a React app that works and a React app that can be operated safely.