Install any skill in seconds. Free to start, no credit card required.
Get Started Free →React frontend security best practices for building secure, production-grade applications. Use when writing or reviewing security-sensitive React code, configuring authentication, handling user input, setting up CSP, securing Server Components/Actions, or hardening a React/Next.js application. Covers OWASP 2025, XSS prevention, CSRF, auth patterns, token storage, Server Components security (CVE-2025-55182), Next.js middleware/proxy hardening, supply chain security, CSP with Vite 6/Next.js 16, Zu
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-12 | ✗→✓ | ▲ Improved | 183% | 0% |
| case-01 | ✗→✓ | ▲ Improved | 217% | 0% |
| case-04 | ✗→✓ | ▲ Improved | 218% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 226% | 0% |
| case-20 | ✗→✓ | ▲ Improved | 201% | 0% |
A comprehensive guide for building secure, production-grade React applications. Covers React 19, Next.js 16, Vite 6, and aligns with OWASP Top 10:2025.
| Library | Status | Best for | | ---------------------- | --------------------------- | -------------------------------------------------------- | | Better Auth | Active, recommended | New projects — TypeScript-first, plugin system, SSO/SAML | | Clerk | Active, hosted | Pre-built UI, managed backend, free up to 10K MAU | | Auth.js (NextAuth) | Maintenance mode | Existing projects only — security patches only | | Lucia Auth | Deprecated (March 2025) | Do not use — plan migration |
typescript// Store tokens in memory only const AuthProvider = ({ children }: { children: ReactNode }) => { const [accessToken, setAccessToken] = useState<string | null>(null); return ( <AuthContext.Provider value={{ accessToken, setAccessToken }}> {children} </AuthContext.Provider> ); }; // Token injection via interceptor api.interceptors.request.use((config) => { const token = getAccessToken(); // from memory/context if (token) config.headers.Authorization = `Bearer ${token}`; return config; }); // Handle 401 with token refresh api.interceptors.response.use( (response) => response, async (error) => { if (error.response?.status === 401) { await refreshAccessToken(); return api.request(error.config); } return Promise.reject(error); } );
An unauthenticated remote code execution vulnerability in React Server Components' Flight protocol. Affected all RSC frameworks (Next.js, React Router, Waku, etc.). Insecure deserialization allowed arbitrary server-side code execution via a single crafted HTTP request.
Affected: react-server-dom-webpack, react-server-dom-parcel, react-server-dom-turbopack (React 19.0–19.2.0). Apps were vulnerable even without directly implementing Server Functions.
Fix: Update to React 19.0.4+, 19.1.5+, or 19.2.4+.
Server Actions are public HTTP endpoints. Treat them as API routes:
typescript"use server"; import { z } from "zod"; import { getSession } from "@/lib/auth"; const updateUserSchema = z.object({ name: z.string().min(1).max(100), email: z.string().email(), }); export async function updateUser(formData: FormData) { // 1. Always authenticate const session = await getSession(); if (!session) throw new Error("Unauthorized"); // 2. Always validate input const parsed = updateUserSchema.safeParse({ name: formData.get("name"), email: formData.get("email"), }); if (!parsed.success) throw new Error("Invalid input"); // 3. Always authorize if (session.user.id !== formData.get("userId")) { throw new Error("Forbidden"); } // 4. Perform the operation await db.user.update({ where: { id: session.user.id }, data: parsed.data }); }
next-safe-action for type-safe Server Actions with built-in validationAttackers could bypass all middleware by sending an x-middleware-subrequest header. Fixed in Next.js 15.2.3+.
middleware.ts renamed to proxy.ts — clarifies this is a network boundary, NOT a security boundary"use cache" directive — include user-bound arguments; call updateTag() after auth/role mutations to avoid stale cached permissionstypescript// proxy.ts import { NextResponse } from "next/server"; import type { NextRequest } from "next/server"; export function middleware(request: NextRequest) { const nonce = Buffer.from(crypto.randomUUID()).toString("base64"); const cspHeader = ` default-src 'self'; script-src 'self' 'nonce-${nonce}' 'strict-dynamic'; style-src 'self' 'nonce-${nonce}'; img-src 'self' data: https:; font-src 'self'; connect-src 'self' https://api.yourdomain.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; `.replace(/\n/g, ""); const response = NextResponse.next(); response.headers.set("Content-Security-Policy", cspHeader); response.headers.set("x-nonce", nonce); return response; }
Next.js automatically extracts the nonce during rendering and applies it to script/style tags.
typescriptconst ProtectedRoute = ({ children, requiredRole, }: { children: ReactNode; requiredRole?: string; }) => { const { user, isAuthenticated } = useAuth(); const location = useLocation(); if (!isAuthenticated) { return <Navigate to="/login" state={{ from: location }} replace />; } if (requiredRole && !user.roles.includes(requiredRole)) { return <Navigate to="/unauthorized" replace />; } return children; };
Important: Client-side route guards are UX convenience only. Always enforce authorization on the server (in Server Actions, API routes, or data access layer).
javascript: protocol injectioneval(), Function())typescriptimport DOMPurify from "dompurify"; import { z } from "zod"; // Schema validation const userSchema = z.object({ email: z.string().email(), age: z.number().min(18).max(120), username: z.string().min(3).max(20).regex(/^[a-zA-Z0-9_]+$/), }); // Sanitize HTML content — encapsulate in a reusable component function SafeHTML({ html }: { html: string }) { const sanitized = DOMPurify.sanitize(html, { ALLOWED_TAGS: ["b", "i", "em", "strong", "a"], ALLOWED_ATTR: ["href"], }); return <div dangerouslySetInnerHTML={{ __html: sanitized }} />; } // File upload validation function validateFile(file: File): boolean { const allowedTypes = ["image/jpeg", "image/png", "image/gif"]; const maxSize = 5 * 1024 * 1024; // 5MB if (!allowedTypes.includes(file.type)) throw new Error("Invalid file type"); if (file.size > maxSize) throw new Error("File too large"); return true; }
dangerouslySetInnerHTMLreact-markdown — converts Markdown to React components without innerHTML; inherently XSS-safehtml-react-parser — parses HTML into JSX elements (still sanitize input first)dangerouslySetInnerHTML in code scanning; always pair with DOMPurifyReact auto-escapes values in JSX by default — {userInput} is safe. The risk is only with dangerouslySetInnerHTML or when constructing HTML strings manually.
Vite has no built-in nonce support. Configure CSP through your server/hosting layer:
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data: https:; font-src 'self'; connect-src 'self' https://api.yourdomain.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'Use nonce-based CSP generated per-request in proxy.ts (see Section 3).
Tailwind v4 generates styles at build time (CSS file output) — compatible with style-src hash or nonce. No inline styles by default, which simplifies CSP.
typescript// ❌ BAD — raw user input <div dangerouslySetInnerHTML={{ __html: userInput }} /> // ✅ GOOD — sanitized <div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(userInput) }} /> // ✅ BETTER — avoid dangerouslySetInnerHTML entirely <div>{userInput}</div> // React auto-escapes
'unsafe-eval' (React debugging tools)'unsafe-eval'X-Requested-With) to AJAX requeststypescript// Configure CSRF in axios axios.defaults.xsrfCookieName = "csrftoken"; axios.defaults.xsrfHeaderName = "X-CSRFToken"; axios.defaults.headers.common["X-Requested-With"] = "XMLHttpRequest";
Server Actions use POST and automatically compare Origin vs Host headers. SameSite cookies provide the default CSRF protection.
typescriptconst maskCreditCard = (cc: string): string => cc.replace(/\d(?=\d{4})/g, "*"); // "1234567890123456" → "************3456" const maskEmail = (email: string): string => { const [local, domain] = email.split("@"); return `${local.slice(0, 2)}***@${domain}`; }; // "user@example.com" → "us***@example.com"
AbortController (axios CancelToken is deprecated)typescriptuseEffect(() => { const controller = new AbortController(); api .get("/users", { signal: controller.signal }) .then(({ data }) => setUsers(data)) .catch((err) => { if (!axios.isCancel(err)) throw err; // Ignore cancellation }); return () => controller.abort(); }, []);
typescriptimport { z } from "zod"; const responseSchema = z.object({ success: z.boolean(), data: z.object({ id: z.string(), name: z.string() }), }); api.interceptors.response.use((response) => { const validated = responseSchema.parse(response.data); return { ...response, data: validated }; });
Supply chain attacks are now a top-3 OWASP category. The npm ecosystem has seen major incidents:
| Date | Incident | | -------- | --------------------------------------------------------------------------------------------------- | | Sep 2025 | CISA alert: 18 popular npm packages (chalk, debug, ansi-styles) compromised via maintainer phishing | | Nov 2025 | Self-replicating npm worm: 796 packages, 132M monthly downloads | | Jan 2026 | "PackageGate": 6 zero-days across npm, pnpm, vlt, Bun undermining lockfile trust |
@fs path traversal bypasspostinstall scripts; use allowBuilds to whitelist trusted packagesnpm audit / pnpm audit in CIhtml<script src="https://cdn.example.com/library.js" integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/ux..." crossorigin="anonymous" ></script>
devtools middleware must be explicitly disabled in productiontypescriptimport { devtools } from "zustand/middleware"; const useStore = create( process.env.NODE_ENV === "development" ? devtools(storeImpl) : storeImpl, );
nonce prop for strict CSP environmentsprocess.env.NODE_ENV check or enabled: falsebash# .env — never commit secrets VITE_API_URL=https://api.example.com # Don't prefix secrets with VITE_ — they get bundled into client code
typescript// vite.config.ts export default defineConfig({ build: { sourcemap: false, // Disable in production minify: "terser", terserOptions: { compress: { drop_console: true, drop_debugger: true, }, }, }, });
Configure at the server/CDN level (not in the app):
Content-Security-Policy: [see sections 3 and 6]
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: geolocation=(), microphone=(), camera=()
Strict-Transport-Security: max-age=31536000; includeSubDomains; preloadNote: Setting security headers via <meta> tags (react-helmet) is a fallback — server-side headers are more reliable and harder to bypass.
typescriptimport * as Sentry from "@sentry/react"; Sentry.init({ dsn: import.meta.env.VITE_SENTRY_DSN, beforeSend(event) { // Filter sensitive data before sending if (event.user) delete event.user.email; return event; }, }); // Error boundary (use Sentry.ErrorBoundary or custom) <Sentry.ErrorBoundary fallback={<ErrorFallback />}> <App /> </Sentry.ErrorBoundary>
| Layer | Tool | | ----------------- | ------------------------------------- | | Unit/Component | Vitest + React Testing Library | | Security scanning | Socket.dev, Snyk, npm audit | | E2E | Playwright | | Accessibility | axe-core | | Performance | Lighthouse |
typescriptimport { render } from "@testing-library/react"; import DOMPurify from "dompurify"; test("sanitizes XSS attempt", () => { const malicious = '<img src=x onerror=alert("XSS")>'; const { container } = render( <div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(malicious) }} /> ); expect(container.querySelector("img")).not.toHaveAttribute("onerror"); }); test("protected route redirects unauthenticated users", () => { render(<ProtectedRoute><Dashboard /></ProtectedRoute>); expect(screen.queryByText("Dashboard")).not.toBeInTheDocument(); });
Where applicable:
| Rank | Category | React Relevance | | ------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------- | | A01 | Broken Access Control | High — client-side guards are UX only; enforce on server | | A02 | Security Misconfiguration | High — CSP, CORS, debug modes in prod | | A03 | Supply Chain Failures (NEW) | Critical — npm ecosystem attacks | | A04 | Cryptographic Failures | Medium — token handling, HTTPS | | A05 | Injection | Lower for React (JSX auto-escapes) — still risk with dangerouslySetInnerHTML and Server Actions | | A06 | Insecure Design | Medium — threat modeling for RSC architecture | | A07 | Auth Failures | High — see Section 1 | | A08 | Software & Data Integrity | High — CI/CD pipeline security | | A09 | Logging & Alerting Failures | Medium — client-side error reporting | | A10 | Mishandling Exceptional Conditions (NEW) | Medium — error boundaries, fail closed |
Before deploying to production:
Other measured skills in the registry, with their headline benchmark lift.