Introduction
React has dominated front-end development for nearly a decade, but a dangerous myth persists: that React is "secure by default." In 2026, with supply chain attacks on the rise and CSP bypass techniques growing more sophisticated, this misconception leaves countless apps vulnerable. React's JSX escaping protects against some XSS, but it does nothing for server-side rendering pitfalls, dependency vulnerabilities, misconfigured security headers, or business logic flaws. A single unvetted dependency or a misplaced dangerouslySetInnerHTML can expose user data. This guide walks through a complete React security audit: XSS prevention, CSP configuration, dependency scanning, OWASP Top 10 mapping, and authentication best practices. Each section includes real code examples and actionable fixes.
1. XSS Prevention in React
The danger of dangerouslySetInnerHTML
React intentionally names dangerouslySetInnerHTML to warn you. It bypasses React's built-in escaping and inserts raw HTML into the DOM. Use it only when rendering trusted HTML — and never with user input.
Vulnerable code:
function Comment({ body }: { body: string }) {
return <div dangerouslySetInnerHTML={{ __html: body }} />;
}
// User submits: <img src=x onerror="fetch('/api/steal?cookie='+document.cookie)">
Fixed with DOMPurify:
import DOMPurify from "dompurify";
function Comment({ body }: { body: string }) {
const clean = DOMPurify.sanitize(body);
return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}
DOMPurify strips malicious constructs while preserving safe HTML. Always configure it with strict rules:
DOMPurify.sanitize(input, {
ALLOWED_TAGS: ["b", "i", "em", "strong", "a", "code", "pre"],
ALLOWED_ATTR: ["href", "class"],
ALLOW_DATA_ATTR: false,
});
React's built-in protection
JSX automatically escapes values in {} — strings become textContent, not innerHTML. This prevents injection in the common case:
function SafeGreeting({ name }: { name: string }) {
return <div>{name}</div>; // Safe: even "<script>alert(1)</script>" is text
}
But beware of props like href, src, or style where React does not escape the same way:
function UserLink({ url }: { url: string }) {
return <a href={url}>Click</a>; // javascript:alert(1) works here!
}
Fix: Validate URLs with a helper:
function isValidUrl(url: string): boolean {
try {
const parsed = new URL(url, window.location.origin);
return parsed.protocol === "http:" || parsed.protocol === "https:";
} catch {
return false;
}
}
function UserLink({ url }: { url: string }) {
if (!isValidUrl(url)) return null;
return <a href={url}>Click</a>;
}
2. CSP Headers for React Apps
Content Security Policy (CSP) is your second line of defense against XSS. It tells the browser what sources of content are trusted.
Configuring CSP
For a Next.js app, set CSP via middleware or next.config.ts:
// next.config.ts
const csp = `
default-src 'self';
script-src 'self';
style-src 'self' 'unsafe-inline';
img-src 'self' https: data:;
connect-src 'self' https://api.yoursite.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
`.replace(/\s{2,}/g, " ").trim();
module.exports = {
async headers() {
return [
{
source: "/(.*)",
headers: [
{ key: "Content-Security-Policy", value: csp },
{ key: "X-Content-Type-Options", value: "nosniff" },
{ key: "X-Frame-Options", value: "DENY" },
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
],
},
];
},
};
For Create React App, use the CRA approach by serving the HTML with the headers set on the server (nginx, Apache, or a CDN like Cloudflare):
# nginx.conf
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' https: data:; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self';" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
Testing your CSP
Use Google's CSP Evaluator to check for policy weaknesses. A policy with 'unsafe-inline' on script-src offers no script injection protection — consider moving inline scripts into separate files or using a nonce/hash-based policy:
// Nonce-based CSP for Next.js
const nonce = crypto.randomBytes(16).toString("base64");
const csp = `default-src 'self'; script-src 'self' 'nonce-${nonce}'; style-src 'self' 'nonce-${nonce}';`;
3. Dependency Security
Your app is only as secure as its weakest dependency. Modern React apps easily pull in 1,000+ packages via the dependency tree.
npm audit and its limitations
npm audit flags known vulnerabilities against the npm Advisory database:
npm audit
But it produces false positives (flagged vulns in dev-only deps) and false negatives (vulnerabilities not yet disclosed or published). It's a baseline, not a substitute.
Supply chain attack case studies
- event-stream (2018): A malicious package
flatmap-streamwas injected into the popularevent-streamnpm package, targeting a specific Bitcoin wallet app to steal private keys. - ua-parser-js (2021): The maintainer's npm credentials were compromised, and malicious code was pushed that installed coin miners.
- node-ipc (2022): Protestware that deleted files in certain regions.
Defense strategy
# Audit every CI run
npm audit --audit-level=high
# Use exact versions for critical deps
# package.json: "react": "19.2.6" (no ^ or ~)
# Verify lockfile integrity
npm audit signatures
For deeper analysis, use Snyk or Socket.dev which go beyond CVEs and analyze package behavior (e.g., network calls, filesystem access, obfuscated code):
npx snyk test
Always review lockfile changes in PRs. A modified package-lock.json or yarn.lock with a suspicious sub-dependency version bump is a red flag.
4. OWASP Top 10 for React
A01: Broken Access Control
React apps expose API routes that must enforce authorization server-side. Never rely on hiding UI elements alone:
// VULNERABLE: only hides the admin panel
function AdminPanel() {
const { user } = useAuth();
if (user.role !== "admin") return null; // Client-side only!
return <AdminDashboard />;
}
Fix: Verify authorization in your API route handler or server action:
// server-side check (Next.js Route Handler)
export async function GET(req: Request) {
const session = await getSession(req);
if (session?.role !== "admin") {
return Response.json({ error: "Forbidden" }, { status: 403 });
}
return Response.json(await getSensitiveData());
}
A03: Injection (XSS)
Covered in Section 1. For GraphQL/REST endpoints, also validate and sanitize all inputs before they reach your database. Use Zod or Valibot for schema validation:
import { z } from "zod";
const commentSchema = z.object({
body: z.string().max(5000),
postId: z.string().uuid(),
});
A05: Security Misconfiguration
Missing CSP headers, exposed .env files, and unnecessary CORS origins are common. Audit your response headers with:
curl -sI https://yoursite.com | grep -iE "content-security-policy|x-frame-options|strict-transport-security"
A06: Vulnerable and Outdated Components
Use npm outdated regularly:
npm outdated
Set up Dependabot or Renovate to automate minor/patch updates. For major upgrades, audit breaking changes manually.
A08: Software and Data Integrity Failures
Subresource Integrity (SRI) for external scripts, lockfile verification, and signed commits protect the integrity of your supply chain:
<script src="https://cdn.example.com/lib.js" integrity="sha384-abc123..." crossorigin="anonymous"></script>
5. Authentication & Authorization
JWT storage
Never store JWTs in localStorage — it's accessible to any JavaScript on the same origin, making it trivially stealable via XSS. Use httpOnly, Secure, SameSite=Strict cookies instead:
// Server-side (Next.js Route Handler)
export async function POST(req: Request) {
const { token } = await req.json();
// Set as httpOnly cookie
const headers = new Headers();
headers.append(
"Set-Cookie",
`session=${token}; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=86400`
);
return Response.json({ ok: true }, { headers });
}
OAuth flows
For OAuth (Google, GitHub, etc.), use the Authorization Code Flow with PKCE, never the Implicit Flow:
// PKCE code verifier generation
const generateCodeVerifier = () => {
const array = crypto.getRandomValues(new Uint8Array(32));
return btoa(String.fromCharCode(...array))
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
};
CSRF protection
If you're using cookie-based auth, CSRF tokens are mandatory unless you set SameSite=Strict. For API routes, use a double-submit cookie pattern or a dedicated CSRF token:
// Next.js middleware CSRF check
import { csrf } from "@/lib/csrf";
export async function middleware(req: NextRequest) {
if (req.method !== "GET") {
await csrf(req); // Throws 403 if invalid
}
return NextResponse.next();
}
6. React Security Audit Checklist
Use this checklist in every audit cycle:
- [ ] CSP headers configured and tested with CSP Evaluator
- [ ]
dangerouslySetInnerHTMLusage reviewed — sanitized with DOMPurify? - [ ]
href/srcprops validated forjavascript:and other dangerous schemes - [ ] All dependencies up to date (
npm outdated,npm audit) - [ ] Lockfile reviewed for unexpected sub-dependency changes
- [ ] Supply chain: Snyk/Socket.dev scan configured in CI
- [ ] JWT stored in
httpOnlycookies (neverlocalStorage) - [ ] CSRF protection implemented for state-changing requests
- [ ] OWASP Top 10 tested — especially A01, A03, A05, A06, A08
- [ ] Environment variables stripped from client bundle (no
NEXT_PUBLIC_*for secrets) - [ ] API endpoints rate-limited and authorization-checked server-side
- [ ] SRI hashes present on external CDN scripts
- [ ] Security headers present:
X-Content-Type-Options,X-Frame-Options,Referrer-Policy
Conclusion
Security is not a one-time checklist item — it's a continuous practice that must keep pace with your application's evolution. Every new dependency, every feature change, and every deployment is an opportunity for a vulnerability to slip in. Integrate automated security scanning into your CI pipeline, review your CSP quarterly, and make security reviews part of your pull request workflow. If you need a thorough, professional assessment of your React application's security posture, contact us for a comprehensive security audit. Our team maps your entire attack surface — from dependency trees to deployment headers — and delivers a prioritized remediation plan.
JS Security Audit
Senior JavaScript security consultants with 10+ years of experience.