Security audit for Next.js apps
Last updated: March 16, 2026 · Written by the RepoVault Security Team
A security audit for a Next.js application is a systematic review of your app's API routes, server components, middleware, authentication flows, environment variables, and dependencies to identify vulnerabilities before they're exploited. Next.js introduces framework-specific security concerns — like server/client boundary leaks and NEXT_PUBLIC_ exposure — that traditional security guides don't cover.
Next.js powers over 1 million websites including production apps handling sensitive user data and payments. The IBM 2024 Cost of a Data Breach Report puts the average breach cost at $4.88 million. For Next.js apps specifically, the most exploited vulnerabilities are ones the framework makes easy to introduce.
The 7 most common Next.js security issues
1. Environment variable exposure via NEXT_PUBLIC_
Any environment variable prefixed with NEXT_PUBLIC_ is bundled into your client-side JavaScript. This is by design — but developers frequently prefix sensitive keys with NEXT_PUBLIC_ because “it works.”
BAD — Secret exposed to client:
# .env.local NEXT_PUBLIC_STRIPE_SECRET_KEY=sk_live_xxx NEXT_PUBLIC_DATABASE_URL=postgresql://... NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY=eyJ...
GOOD — Only safe values are public:
# .env.local NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ... STRIPE_SECRET_KEY=sk_live_xxx # server-only DATABASE_URL=postgresql://... # server-only SUPABASE_SERVICE_ROLE_KEY=eyJ... # server-only
2. Unprotected API routes
Every file in app/api/ is a publicly accessible endpoint. Without authentication checks, anyone can call them.
BAD — No auth check:
// app/api/users/route.ts
export async function GET() {
const users = await db.query("SELECT * FROM users");
return Response.json(users);
}GOOD — Auth verified first:
// app/api/users/route.ts
export async function GET(request: Request) {
const session = await getServerSession();
if (!session) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}
const users = await db.query(
"SELECT id, name FROM users WHERE org_id = $1",
[session.user.orgId]
);
return Response.json(users);
}3. Server component data leaks
Server components run on the server but their return values are serialized and sent to the client. If you fetch sensitive data in a server component and pass it as props, it's visible in the page source.
BAD — Sensitive data in server component props:
// app/dashboard/page.tsx (server component)
export default async function Dashboard() {
const user = await db.query("SELECT * FROM users WHERE id = $1", [userId]);
// user.password_hash, user.stripe_customer_id, etc.
// are now serialized to the client
return <DashboardClient user={user} />;
}GOOD — Only send safe fields:
// app/dashboard/page.tsx (server component)
export default async function Dashboard() {
const user = await db.query(
"SELECT id, name, email, plan FROM users WHERE id = $1",
[userId]
);
return <DashboardClient user={user} />;
}4. Middleware bypasses
Next.js middleware runs before route handlers, making it ideal for authentication. But misconfigured matcher patterns can leave routes unprotected.
BAD — Incomplete matcher:
// middleware.ts
export const config = {
matcher: ["/dashboard", "/settings"],
// Missing: /admin, /api/admin/*, etc.
};GOOD — Catch-all with explicit exclusions:
// middleware.ts
export const config = {
matcher: [
"/((?!_next/static|_next/image|favicon.ico|public/).*)",
],
};5. Missing security headers
Next.js doesn't set security headers by default. You need to configure them in next.config.js.
// next.config.js
const securityHeaders = [
{ key: "X-Content-Type-Options", value: "nosniff" },
{ key: "X-Frame-Options", value: "DENY" },
{ key: "X-XSS-Protection", value: "1; mode=block" },
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
{ key: "Strict-Transport-Security",
value: "max-age=63072000; includeSubDomains; preload" },
{ key: "Content-Security-Policy",
value: "default-src 'self'; script-src 'self' 'unsafe-inline';" },
];
module.exports = {
async headers() {
return [{ source: "/(.*)", headers: securityHeaders }];
},
};6. Insecure Server Actions
Server Actions in Next.js create POST endpoints automatically. Without validation, they're vulnerable to the same injection attacks as any API route.
BAD — No validation in Server Action:
"use server";
export async function updateProfile(formData: FormData) {
const name = formData.get("name") as string;
await db.query(`UPDATE users SET name = '${name}'`);
}GOOD — Validated and parameterized:
"use server";
import { z } from "zod";
const schema = z.object({ name: z.string().min(1).max(100) });
export async function updateProfile(formData: FormData) {
const { name } = schema.parse({
name: formData.get("name"),
});
await db.query("UPDATE users SET name = $1 WHERE id = $2", [name, userId]);
}7. Outdated dependencies
Next.js apps typically have 200+ npm packages. Each is a potential attack vector. Snyk reports that 92% of applications contain at least one vulnerable dependency.
How to audit your Next.js app step-by-step
Step 1: Audit API routes for input validation
Check every file in app/api/ for proper input validation and sanitization. Ensure all request body parsing uses schema validation. Verify SQL queries use parameterized statements.
Step 2: Check server components for data leaks
Review server components to ensure sensitive data isn't accidentally serialized to the client. Check for "use server" directives on sensitive functions.
Step 3: Verify middleware authentication
Ensure middleware.ts properly protects all authenticated routes. Check that the matcher config covers every protected path.
Step 4: Review environment variable exposure
Search your codebase for NEXT_PUBLIC_ variables. Verify that only public-safe values use this prefix.
Step 5: Scan dependencies
Run npm audit and address critical and high-severity findings.
Step 6: Add security headers
Configure the six essential security headers in your next.config.js.
Step 7: Run an automated scan with RepoVault
RepoVault scans your entire Next.js repository in 60 seconds and catches all the issues above — plus framework-specific vulnerabilities that generic scanners miss. Get a security score, severity-ranked findings, and one-click fixes.
Frequently asked questions
What are the most common Next.js security vulnerabilities?
The most common Next.js security vulnerabilities are: exposed environment variables via NEXT_PUBLIC_ prefix, unprotected API routes without authentication checks, server component data leaks, missing security headers, middleware bypasses, and insecure Server Actions without proper validation.
How do I secure Next.js API routes?
Secure Next.js API routes by adding authentication checks at the start of every route handler, validating all input with a schema library like Zod, using parameterized queries for database operations, adding rate limiting middleware, setting proper CORS headers, and never exposing sensitive error details in responses.
Can NEXT_PUBLIC_ environment variables expose secrets?
Yes. Any environment variable prefixed with NEXT_PUBLIC_ is bundled into your client-side JavaScript and visible to anyone who inspects your page source. Never use NEXT_PUBLIC_ for API secret keys, database URLs, webhook secrets, or service role keys.