By Ahmad Sadeddin

Next.JS Security Best Practices - 2025

A comprehensive guide to securing your Next.JS applications

security
best-practices
next.js
javascript

nextjs title image

Securing Your Next.js App in 2025: Best Practices with Real-World Examples


As Next.js matures into a full-stack framework, developers enjoy unparalleled flexibility---but with great power comes the potential for serious security pitfalls. In this guide, we'll explore modern, practical security best practices specifically tailored for Next.js in 2025. Each section includes actionable examples of what to do and what not to do and touches on how tools like Corgea, a static application security testing (SAST) platform, can proactively catch issues before they make it to production. Next.js also has a great article on how to think about security, check it out here.


1. Never Leak Server-Only Logic to the Client

One of the most fundamental---and dangerous---mistakes in Next.js development is accidentally exposing server-only data or logic to the client. This typically happens when developers assume that environment variables or sensitive functions used in any file will be kept server-side. However, any code outside of server-only functions like getServerSideProps, getStaticProps, or API routes is likely to be bundled into the client-side JavaScript**.

To avoid leaking secrets or internal logic, you need a clear mental model of the boundary between client and server in your Next.js app.

✅ What You Should Do:

// pages/api/secret.ts
export default function handler(req, res) {
  const secret = process.env.MY_SECRET_KEY;
  res.status(200).json({ message: "This stays on the server." });
}

❌ What You Should Not Do:

// pages/index.tsx (client-side code)
const secret = process.env.MY_SECRET_KEY;
console.log("Secret:", secret); // 🚨 This gets bundled and leaked to the browser

Even referencing process.env.MY_SECRET_KEY in client-side code---even if you don't log or display it---can expose it during the build.

How Corgea Helps: Corgea automatically detects usage of server-only environment variables in client-bound files. It uses static analysis to trace sensitive data flows and flag insecure access patterns during code review or CI.


2. Use Taint Tracking to Trace Unsafe Input Flows

The Taint API is a newer browser-native mechanism for tracking potentially unsafe data flows---especially useful when tracing user input across rendering logic.

✅ Correct Usage:

const input = document.querySelector("#email");
const tainted = new Taint(input.value);
if (tainted.hasTaint()) {
  alert("Sanitize this input before use");
}

❌ What Not to Do:

const input = document.querySelector("#email");
const html = `<div>\${input.value}</div>`;
document.body.innerHTML = html; // Unsanitized injection

How Corgea Helps: Corgea identifies unsanitized user inputs flowing into dangerous sinks like innerHTML, even without the Taint API explicitly used.


3. Validate All Inputs, Always

"Trust nothing, validate everything" is more than a motto. Whether it's a form submission or an API payload, never trust user input. Next.js gives you multiple places to validate inputs---client-side forms, server actions, and API routes. You should implement validation at every layer, with server-side validation being mandatory.

3.1 Don't Rely on Client-Side Checks for Sensitive Operations

In Next.js App Router, it's tempting to mix server and client logic. But doing so incorrectly can expose dangerous functionality to attackers.

Consider the following insecure code posted by this Redditor:

export default async function AdminPage() {
  const userIsAdmin = await isAdmin();
  if (!userIsAdmin) throw new Response("Unauthorized", { status: 401 });
  return (
    <Button
      onClick={async () => {
        "use server"; 
        await orm.records.deleteMany();
      }}
    >
      DELETE IMPORTANT STUFF
    </Button>
  );
}

What to do instead:

  1. Move sensitive logic to a real server action or API route.
  2. Always recheck authorization on the server—never assume a previous check is still valid.

3.2 Use Zod for Schema Validation

Zod provides a powerful, type-safe way to validate data in your Next.js app.

✅ Correct Usage:

import { z } from 'zod';

const userSchema = z.object({
  name: z.string(),
  email: z.string().email(),
});

const data = userSchema.parse(req.body);

❌ What Not to Do:

const { name, email } = req.body;
if (!name || !email.includes("@")) {
  throw new Error("Invalid data"); // Weak validation
}

3.3 Validate Server Actions in App Router

In the App Router (app/ directory), server actions need just as much validation as API routes.

✅ Correct Usage:

// app/actions.ts
import { z } from 'zod';

const schema = z.object({ query: z.string().min(1) });

export async function search(data: FormData) {
  const parsed = schema.safeParse({ query: data.get("query") });
  if (!parsed.success) throw new Error("Invalid input");
  // safe to proceed
}

❌ What Not to Do:

export async function search(data: FormData) {
  const query = data.get("query");
  // blindly trust input
  return await db.search(query);
}

3.4 Validate Next.js API Routes

Even if your frontend already validates data, server-side routes must re-validate it.

✅ Correct Usage:

// pages/api/submit.ts
const schema = z.object({ email: z.string().email() });

export default function handler(req, res) {
  const parsed = schema.safeParse(req.body);
  if (!parsed.success) return res.status(400).json({ error: "Invalid" });
  res.status(200).json({ status: "OK" });
}

❌ What Not to Do:

export default function handler(req, res) {
  const { email } = req.body;
  // no validation
  saveToDb(email);
  res.status(200).end();
}

4. Implement a Strict Content Security Policy (CSP)

A strong Content Security Policy (CSP) helps prevent XSS by disallowing inline scripts or loading from untrusted sources.

✅ Correct Usage:

// next.config.js
const ContentSecurityPolicy = `
  default-src 'self';
  script-src 'self';
  object-src 'none';
  base-uri 'none';
`;

module.exports = {
  headers() {
    return [
      {
        source: '/(.*)',
        headers: [{ key: 'Content-Security-Policy', value: ContentSecurityPolicy }],
      },
    ];
  },
};

❌ What Not to Do:

// No CSP defined
// Or allowing unsafe-inline
script-src 'self' 'unsafe-inline';

5. Manage Environment Variables the Right Way

Only expose public environment variables that are explicitly meant for the browser using the NEXT_PUBLIC_ prefix.

✅ Correct Usage:

// .env
NEXT_PUBLIC_MAPBOX_TOKEN=abc123 // intended for frontend

// pages/index.tsx
console.log(process.env.NEXT_PUBLIC_MAPBOX_TOKEN); // OK

❌ What Not to Do:

// .env
SECRET_DB_PASSWORD=shhh

// pages/index.tsx
console.log(process.env.SECRET_DB_PASSWORD); // Gets bundled in client!

How Corgea Helps: Detects unsafe usage of secrets in code paths likely to be exposed to the client.


6. Secure Authorization: Lock Down Access by Role

Auth checks must be enforced on the server side, especially in API routes and server actions.

✅ Correct Usage:

export default function handler(req, res) {
  const session = getSession(req);
  if (!session || session.user.role !== 'admin') {
    return res.status(403).json({ error: "Access denied" });
  }
  // Safe to proceed
}

❌ What Not to Do:

// Only hiding UI based on role on the frontend
if (user.role === 'admin') {
  showDeleteButton();
}
// No server check --- insecure!

7. Don't Rely Solely on Middleware

While Next.js middleware can be useful for redirects and light auth checks, it should never be your only gatekeeper.

✅ Correct Usage:

// middleware.ts
export function middleware(req) {
  const token = req.cookies.get('token');
  if (!token) return NextResponse.redirect('/login');
  return NextResponse.next();
}

// Plus server-side role checks in API routes/actions

❌ What Not to Do:

// Only using middleware
export function middleware(req) {
  if (!req.cookies.get('token')) return NextResponse.redirect('/login');
}
// And assuming that's enough for security

Wrapping Up: Catch Issues Early with SAST Tools like Corgea

As your Next.js app grows, so does the surface area for potential security vulnerabilities. Best practices like validation, CSPs, and strict role checks are essential---but even experienced teams can miss subtle bugs. This is where a modern static analysis tool like Corgea steps in.

Corgea scans your Next.js codebase to detect:

  • Leaked environment variables

  • Unvalidated user inputs

  • Tainted data flows to dangerous sinks

  • Authorization gaps in server code

Security is a process, not a patch. And with the right tools and discipline, you can ship secure, production-grade Next.js apps with confidence.


Stay safe, and secure your stack!

Corgea Logo

Find and fix vulnerabilities with Corgea

Scan your codebase and get fixes instantly.

Start for free and no credit card needed.