DevCerts logo DevCerts

React Security in Production: XSS, Tokens, CSP, and npm Supply-Chain Risk

React escapes a lot by default, but production security failures usually happen at the edges: token storage, unsafe HTML rendering, weak CSP, and unreviewed npm dependencies. This article turns those risks into practical engineering decisions.

React
React Security in Production: XSS, Tokens, CSP, and npm Supply-Chain Risk

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 dangerouslySetInnerHTML

  • Direct 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 sources

  • connect-src, because it controls where frontend code can send data

  • frame-ancestors, because it controls clickjacking exposure

  • object-src 'none', because plugin content is rarely needed

  • base-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 outdated

Those 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 ci

  • Review 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.json

  • Prefer 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.

  • dangerouslySetInnerHTML is 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-ancestors is set deliberately.

  • Production uses HTTPS-only cookies with appropriate SameSite and Secure attributes.

  • 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.

  1. Remove persistent tokens from localStorage where practical.

  2. Centralize and test all raw HTML rendering.

  3. Add a CSP that starts in report-only mode, then enforce it after tuning.

  4. Lock down dependency installation in CI.

  5. 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.