← Back to blogGUIDE

Security audit for Next.js apps

Mar 16, 2026·10 min read

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.

Scan your app for these vulnerabilities →

Free · 60 seconds · No account required

Scan for Free