
How to Secure React.js Applications

React.js powers a significant share of modern web applications, but its flexibility is a double-edged sword. The same composability that makes it fast to build with also makes it easy to introduce security holes without noticing. This post covers the specific attack vectors that matter most in React apps and what you should actually do about them.
What Are the Most Common Security Vulnerabilities in React Applications?
React's JSX auto-escapes output by default, which stops most naive XSS attempts. The problems start when developers bypass that protection without realising it, or when they trust third-party packages to be clean.
Cross-Site Scripting (XSS)
The main culprit is dangerouslySetInnerHTML. It exists for a reason — rendering server-side HTML or rich text from a CMS — but using it without sanitisation is an open invitation. If you must use it, run the content through DOMPurify before it reaches the DOM.
import DOMPurify from 'dompurify';
const clean = DOMPurify.sanitize(rawHTML);
<div dangerouslySetInnerHTML={{ __html: clean }} />
Also watch for href attributes populated from user input. React does not block javascript: URIs in anchor tags in older versions. From React 16.9 onwards, it logs a warning but still renders them in some scenarios, so validate URL schemes explicitly.
Dependency Chain Risks
The average React project pulls in somewhere between 500 and 1,500 transitive npm packages. Each one is a potential supply-chain risk. The 2021 ua-parser-js compromise and the 2022 node-ipc incident showed how quickly a single malicious package can propagate. Run npm audit as part of CI, pin versions in package-lock.json, and consider Socket.dev or Snyk for continuous monitoring beyond what audit catches.
Insecure Direct Object References via the API Layer
React itself doesn't cause IDOR, but React SPAs make it easy to hide access control logic in the frontend, which is then trivially bypassed. Every data-fetching call needs authorisation enforced on the server. A component that conditionally renders an "admin" button is not a security control.
How Should You Handle Authentication and Token Storage?
This is where most teams make a consequential decision early on and then live with the consequences for years.
JWT Storage: localStorage vs httpOnly Cookies
The standard advice is to avoid storing JWTs in localStorage because XSS can read it. That's true. httpOnly cookies are not accessible to JavaScript, which removes that vector. The trade-off is that cookies introduce CSRF risk, which you then need to mitigate with SameSite=Strict or SameSite=Lax and a CSRF token for state-changing requests.
| Storage method | XSS risk | CSRF risk | Works across subdomains |
|---|---|---|---|
localStorage |
High | None | Yes |
sessionStorage |
High | None | No |
httpOnly cookie |
None | Yes (mitigable) | Configurable |
| In-memory (React state) | Low | None | No (lost on refresh) |
In-memory storage is underused. Store the access token in a closure or React context, use a silent refresh via a httpOnly refresh token cookie, and you get a reasonable security posture without the localStorage exposure. Libraries like react-oidc-context handle this pattern for OIDC flows.
Session Expiry and Token Rotation
Set short expiry on access tokens — 15 minutes is a common default in production systems. Implement refresh token rotation: each refresh issues a new refresh token and invalidates the old one. If a stolen token is used, the legitimate user's next refresh will fail and they get logged out, at which point you know something is wrong.
/// Not sure where to start?
Get the architecture before you commit
Tell us what you're building and we'll map the technical approach, stack, and rough timeline. No cost, no obligation, no sales call required.
Protecting Against CSRF and Clickjacking
CSRF is less of a concern for pure API-driven SPAs that use Authorization headers with bearer tokens, because cookies aren't involved in those requests. The moment you switch to cookie-based auth, CSRF becomes relevant again.
Set your API responses with SameSite=Strict on session cookies where possible. For cases where cross-site requests are legitimate, implement the double-submit cookie pattern or use synchronised token patterns.
Clickjacking is handled at the HTTP header level, not the React level. Set X-Frame-Options: DENY or use Content-Security-Policy: frame-ancestors 'none' on your server responses. React has no built-in mechanism for this.
What Does a Practical Content Security Policy Look Like for a React SPA?
CSP is one of the highest-leverage controls you can add, and it's also the one most teams get wrong or skip entirely because it breaks things.
The core problem with React and CSP is inline scripts. Create React App (and to a lesser extent Vite) historically injected runtime scripts inline, which requires 'unsafe-inline' in your policy, which defeats much of the point. The modern approach is to use nonces generated server-side and injected per request, or to move to a build setup that externalises all scripts.
A production-grade starting policy for a React app might look like:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{RANDOM}';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
connect-src 'self' https://api.yourdomain.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
Start in Content-Security-Policy-Report-Only mode with a report-uri endpoint (or use report-uri.com). Collect violations for a week before enforcing. You will find things you didn't know were there.
Environment Variables and Secrets in the Frontend
Anything prefixed with REACT_APP_ in a CRA project, or VITE_ in a Vite project, is bundled into the client-side JavaScript. It is not a secret. Anyone who opens DevTools can read it.
This sounds obvious, but the number of production builds with API keys, Stripe publishable keys used incorrectly, or internal endpoint URLs embedded in bundles is not small. The rule is simple: if a value needs to stay private, it belongs on the server.
Use your backend as a proxy for third-party API calls where keys must be kept private. For values that are genuinely public but environment-specific (base URLs, feature flags), environment variables are fine.
Conclusion
The most impactful things you can do right now are: audit your use of dangerouslySetInnerHTML, move token storage away from localStorage, set a Content Security Policy in report-only mode and start collecting data, and run npm audit in your CI pipeline if you're not already.
Pick one of those and fix it this week. Security improvements done incrementally and consistently are more effective than a one-time audit followed by months of nothing.
FAQ
Does React protect against XSS automatically?
React escapes values in JSX expressions by default, which prevents most reflected XSS. However, dangerouslySetInnerHTML, href attributes with user-supplied values, and eval-based patterns all bypass this protection. Auto-escaping covers the common case, not all cases. You still need explicit sanitisation in those scenarios.
Is it safe to store a JWT in localStorage?
Not ideally. Any JavaScript running on the page — including from a third-party script — can read localStorage. If you have an XSS vulnerability anywhere on the domain, the token is exposed. httpOnly cookies with proper CSRF mitigation, or in-memory storage with refresh token rotation, are more secure alternatives.
What is the biggest React-specific security mistake teams make? Putting access control logic in components. Hiding a button or route in the UI based on a user role feels like security, but it is purely cosmetic. Anyone with DevTools can modify frontend state or call your API directly. Authorisation must be enforced server-side on every request, without exception.
How often should I run a dependency audit?
Every CI build, not just periodically. Use npm audit --audit-level=high to fail builds on high or critical vulnerabilities. Supplement this with a tool like Snyk or Dependabot for automated pull requests when vulnerabilities are disclosed in packages you're using.
Does Content Security Policy break React applications? It can, particularly with older setups that rely on inline scripts. The fix is to move to a nonce-based approach or ensure your build output externalises all scripts. Start in report-only mode to understand what your current policy would block before enforcing it. The setup cost is real but one-time.
Have a project in mind? Contact Sodio Technologies to discuss your requirements and explore the right technology solution for your business.
/// Work with us
Talk to the engineers who'd build it
You'll get a technical scope, timeline and cost estimate from the people doing the work, not an account manager. In-house team, no subcontracting, since 2016.
