← Back to Blog
ReactSecurityAuditOWASP

React Security Audit: A Complete Guide for Developers

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:

tsx
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:

tsx
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:

tsx
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:

tsx
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:

tsx
function UserLink({ url }: { url: string }) {
  return <a href={url}>Click</a>; // javascript:alert(1) works here!
}

Fix: Validate URLs with a helper:

tsx
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:

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
# 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:

ts
// 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:

bash
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-stream was injected into the popular event-stream npm 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

bash
# 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):

bash
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:

tsx
// 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:

ts
// 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:

ts
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:

bash
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:

bash
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:

html
<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:

ts
// 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:

ts
// 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:

ts
// 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
  • [ ] dangerouslySetInnerHTML usage reviewed — sanitized with DOMPurify?
  • [ ] href/src props validated for javascript: 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 httpOnly cookies (never localStorage)
  • [ ] 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.

Share this article:TwitterLinkedIn
JS

JS Security Audit

Senior JavaScript security consultants with 10+ years of experience.