How to secure a React frontend before production
And on December 3, 2025, React published a critical React Server Components advisory, GHSA-fv66-9v8q-g76r. Patch the framework boundary and keep using React.
And React’s published advisory history contains eight disclosures from December 2025 through July 2026. Use a simple ship/no-ship rule: React’s defaults may clear one rendering concern, but a visible secret, unexplained injection sink, broken authentication flow, or applicable unpatched high-severity framework issue blocks release.
But React’s JSX escaping gives you a useful baseline for the security review.
You’ll review React’s rendering model, browser-visible secrets, token storage and OAuth. You’ll also check dependencies and deployed headers. Then you’ll get a release gate based on blockers for the actual production artifact.
In this article
- React is safe to use; your application boundary still needs proof
- Map the browser, server, and delivery boundaries
- JSX escapes ordinary values; inspect every escape hatch
- A client-visible secret blocks the release
- Persist harmless state; keep bearer tokens out of localStorage
- OAuth needs PKCE and validated authentication state
- Patch React’s server boundary and review npm as executable input
- CSP limits browser behavior after code ships
- Use this release gate before production
React is safe to use; your application boundary still needs proof
React’s documentation says, “Every React commit is tested on business-critical surfaces with over a billion users. Over 100,000 React components at Meta help validate every migration strategy.” That is useful evidence of library maturity. It is not application approval.
Your code controls untrusted HTML. Your build controls the configuration sent to the browser. Your API controls invoice access. Dependencies run during installation and execution. Your hosting platform sets the headers that browsers receive.
The practical question is whether your build uses RSC, Server Functions, or another affected integration. If it does, check the applicable fixed versions before approving the release. If it ships only a client-side bundle, the listed RSC and Server Functions advisories generally do not describe that browser bundle; verify whether your framework still runs affected server components in production.
The usual React checklist is badly organized. It mixes browser exposure, API authorization, and mobile behavior as if they were one problem. They are different boundaries, with different owners and different tests.
React protects some rendering paths. It does not protect your application’s secrets, authorization model, package supply chain, or deployment.
Map the browser, server, and delivery boundaries
Start by checking three surfaces:
| Surface | What exists at this boundary | Your release review must test |
|---|---|---|
| React-rendered content | Ordinary JSX interpolation escapes values according to React’s rendering rules | HTML sinks, unsafe URL schemes, dynamic code execution |
| Browser-exposed application | The browser runs bundles and makes network requests | Bundles, source maps, environment variables, storage, URLs, tokens, personal data |
| Server and delivery layer | Your server and platform can hold secrets and enforce access | API authorization, rate limits, RSC/Server Functions versions, install scripts, HTTPS, CSP, headers |
This model keeps responsibilities separate. The browser can display an authorization state; it cannot keep a value confidential after receiving it. Your server can protect a provider key; it must still authorize each request.
OWASP Bullet-proof React is an Incubator Project at version 0.0.0. Use it as a useful collection of guidance, not as a certification or finished approval framework. Your gate must name the evidence for each risk.
OWASP’s project page currently lists Top 10:2025 as the current edition. Treat pages advertising a separate 2026 edition cautiously; Security Boulevard’s discussion likewise reports no new OWASP Top 10 for 2026.
Generic OWASP name-checking earns no approval here; a test tied to the deployed artifact does.
For every finding, ask four questions. Which boundary failed? What can an attacker do? What changed? Which test demonstrates the change?
JSX escapes ordinary values; inspect every escape hatch
React escapes values interpolated into ordinary JSX:
<p>{displayName}</p>
If displayName contains markup, React renders it as text. Keep that default path whenever possible.
Search the repository for dangerouslySetInnerHTML, eval(), and new Function(). Check dynamically assembled href or src values, Markdown or HTML renderers, URL schemes that can become javascript:, and third-party components that accept raw HTML.
// Safe default: the user-provided name is rendered as text
<h2>{displayName}</h2>
// Unsafe unless invoice.notes has been deliberately sanitized
<div dangerouslySetInnerHTML={{ __html: invoice.notes }} />
Remove dangerouslySetInnerHTML when the feature does not require HTML. When the sink remains, require a documented reason, an input policy, and tests for the sanitizer you chose. Use that sanitizer’s documented allowlist rather than assuming there is one universal safe set of elements and attributes.
Test the cases your renderer permits. Check executable URL schemes, event-handler attributes and dangerous SVG or HTML attributes. Test rendered links, images and frames. data: URLs deserve explicit treatment when the feature does not require them.
“Stored in our database” does not mean trusted. A database commonly contains content submitted by users, integrations, or imported files.
Review links as carefully as HTML. A user-controlled URL is data that needs validation, even when JSX renders the surrounding element safely.
Server-side validation and authorization remain mandatory. Hiding a button in React changes the interface. It does not change who may call the API.
A client-visible secret blocks the release
Consider a React dashboard where users upload invoices to a document-processing provider. The tempting design sends the request directly from the browser, which means the private provider key must reach the browser through a request, bundle, or generated configuration.
React Native’s security documentation states the rule: “Never store sensitive API keys in your app code. Anything included in your code could be accessed in plain text by anyone inspecting the app bundle.”
If a secret is in a React bundle, the release is blocked. .env naming is build plumbing, not protection. In many React toolchains, client-targeted environment variables are inlined during the build; verify your framework’s behavior and inspect the artifact. Renaming a variable .env is configuration hygiene, not secret management.
Use one concrete boundary:
- The React client uploads the invoice to your serverless function.
- The function authenticates the user and checks authorization.
- The function authorizes the requested invoice and tenant on the server; never rely on an invoice ID or hidden UI state.
- The function reads the private provider key from server-side configuration.
- It calls the document-processing API and returns only the permitted result.
React client → Blackhawk-controlled serverless function → third-party invoice API
↑
private API key
[IMAGE PLACEHOLDER: React client sends invoices to a Blackhawk-controlled serverless function, which alone holds the secret and calls the third-party invoice API]
The function still needs request limits, input validation, authorization, and careful error handling. Your proxy still needs those controls; forwarding every request to everyone produces an expensive leak.
Before release, inspect JavaScript bundles and source maps, generated public configuration, browser network requests and storage. Check error responses and client-visible hostnames too. Omit production source maps or restrict access to them according to your debugging policy. In either case, check them for credentials.
A public client identifier may be designed for exposure; give it only the privileges intended for public clients.
Persist harmless state; keep bearer tokens out of localStorage
localStorage survives reloads, which makes it convenient. Any JavaScript running in the page can read a token stored there; a compromised dependency is one way that code could arrive.
Memory-only storage removes the persisted copy after reload; it does not protect a live page from XSS.
A cookie-based session can fit better when the server owns the design. HttpOnly prevents page JavaScript from directly reading the cookie, while Secure restricts transmission to HTTPS. You still need server-side authorization and CSRF defenses appropriate to the cookie and request design.
Use this decision rule:
| Data | Release decision |
|---|---|
| Theme, dismissed notices, non-sensitive preferences | Persistence is usually acceptable |
| Cache data | Persist only data you would be willing to disclose to someone who can read the browser profile; exclude credentials and sensitive invoice contents |
| Access or refresh tokens | Do not persist by default; prefer a server-managed session or carefully designed memory-only flow |
| Invoice contents and sensitive drafts | Persist only after a defined retention and protection decision |
| Entire Redux or client state tree | Do not serialize automatically |
Inspect persistence configuration for a root reducer or serialized state path that includes credentials, invoice data, or user details. Review Sentry, Crashlytics, logs, analytics and error payloads. Look for authorization headers, tokens, invoice contents and personal information.
This guide can tell you what to inspect, but it cannot certify your API authorization or identity-provider configuration from the frontend. Test those flows against the deployed system. Screenshots and a clean scanner report are weak evidence.
For the invoice dashboard, inspect storage after login, upload an invoice in the production artifact, trigger a controlled error, and review the telemetry payload.
OAuth needs PKCE and validated authentication state
For a React frontend using OAuth, verify authorization-code flow with Proof Key for Code Exchange (PKCE).
The sequence is:
- The client generates a random
code_verifier. - It derives an
S256code challenge by applying SHA-256 to the verifier and encoding the result as required by the protocol. - It sends the challenge with the authorization request.
- The identity provider returns an authorization code.
- The client sends the original verifier during token exchange.
- The identity provider compares the verifier with the stored challenge.
Require S256 and reject absent or weaker code-challenge methods when your provider supports that policy. A stolen authorization code is useless without the verifier.
Use HTTPS redirect URIs and exact registered addresses. Validate state for authorization-flow CSRF protection. For OpenID Connect, validate the nonce first. Then check the ID-token signature, issuer and audience. OAuth authorization-code validation centers on the client, redirect URI, state, and token endpoint flow; the OpenID Connect checks apply when you are validating an identity token.
React Native’s security guidance warns that mobile deep links are not secure because URL schemes lack centralized registration and another application may claim the same scheme. That qualification belongs to mobile deployments. The web-relevant control is PKCE: a redirect transports the authorization response; your client and server must validate the resulting authentication state.
Patch React’s server boundary and review npm as executable input
We do not approve a build merely because npm audit is clean. (For the server side of the stack, see our guide to securing a Django app before production.) Audit output is one signal; the lockfile, install behavior, and framework boundary still require review. A green npm audit badge is convenient dashboard furniture; it is not a release decision.
Check the packages that define execution: react and react-dom, Next.js or another React framework, RSC packages, Server Functions and related server integrations, authentication packages, and application-specific packages. Compare them with the current advisories on React’s security page.
The 2025–2026 advisory wave includes one critical RSC issue and several high-severity denial-of-service issues. If your deployment ships only a client-side bundle, the listed RSC and Server Functions advisories generally do not describe that browser bundle; verify whether your framework still runs affected server components in production.
The research notes describe Sherlock’s two React vulnerabilities in OSV on August 15, 2026, as an active-vulnerability view. Use that figure as a separate signal from GitHub’s historical total of published advisories, which includes patched issues.
safeguard.sh cites incidents involving ua-parser-js, coa/rc, and a 500-package npm worm. Treat those as supply-chain examples. Turn the lesson into controls. Commit and review the lockfile, then install from it in CI. Remove unused packages and inspect install scripts and unfamiliar additions. Audit direct and transitive dependencies. Investigate high-severity findings instead of suppressing them automatically. Document a compensating control when a fix cannot land before release.
A framework vulnerability and a React-core vulnerability are separate classifications. The release process must catch both.
The hardest part to verify from a frontend review is provider-side authorization; you’ll need deployed-system tests for that, and no clean bundle scan can substitute for them.
CSP limits browser behavior after code ships
CSP is often added before the underlying leak is fixed. That order produces reassuring headers without repairing the exposure.
Capture the deployed response headers. Review HTTPS enforcement, framing protection where appropriate and content-type handling. Check the source-map policy separately. Build CSP from the application’s actual script, connection, image, font, and frame origins. Run it in report-only mode first, retain the violation report, and record the disposition of each legitimate violation before enforcement.
A copied universal CSP is a poor release artifact. It may break a legitimate deployment or permit more than the application needs.
CSP adds a browser-side control layer. It cannot remove a private key from a bundle, repair API authorization, sanitize unsafe HTML, or make a compromised dependency trustworthy. Fix those failures first, then add CSP as defense in depth.
Use this release gate before production
Stop immediately when:
- a private credential or bearer token is client-visible;
- a high-impact HTML or code-execution sink lacks a reason, policy, and test;
- OAuth lacks PKCE where the flow requires it;
- an applicable high-severity React, RSC, Server Functions, or framework advisory remains unresolved without a documented compensating control.
For every other item, assign a named owner and require concrete evidence:
- Code: repository search results, unsafe-sink review and URL-scheme tests. Include server authorization tests.
- Secrets and data: bundle and source-map inspection, storage inventory, network trace, and telemetry-redaction test.
- Authentication: deployed login, logout, expiry, refresh, redirect,
state, and PKCE tests. - Dependencies and delivery: reviewed lockfile diff, CI lockfile installation, audit output, and package-script review. Also record the advisory disposition, deployed headers, HTTPS redirect test, CSP report-only results, and source-map access decision.
Attach the bundle scan, storage and telemetry review, PKCE and authentication tests, lockfile disposition, and deployed-header capture to the release. If one blocker lacks evidence, keep the build out of production.