Next.JS Security Best Practices - 2025
A comprehensive guide to securing your Next.JS applications
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:
- Move sensitive logic to a real server action or API route.
- 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!
On This Page
- Securing Your Next.js App in 2025: Best Practices with Real-World Examples
- 1. Never Leak Server-Only Logic to the Client
- 2. Use Taint Tracking to Trace Unsafe Input Flows
- 3. Validate All Inputs, Always
- 3.1 Don't Rely on Client-Side Checks for Sensitive Operations
- 3.2 Use <a href="https://zod.dev/" target="_blank" rel="noopener noreferrer">Zod</a> for Schema Validation
- 3.3 Validate Server Actions in App Router
- 3.4 Validate Next.js API Routes
- 4. Implement a Strict Content Security Policy (CSP)
- 5. Manage Environment Variables the Right Way
- 6. Secure Authorization: Lock Down Access by Role
- 7. Don't Rely Solely on Middleware
- Wrapping Up: Catch Issues Early with SAST Tools like Corgea