Technical~8 min · intermediate

The basic security holes in a Next.js app

Most holes I find in audits aren't sophisticated: they're oversights. Here are the five most common gaps in a Next.js app, and the fix for each — before someone finds them for you.

StackNext.jsserver-onlyauthIDORXSSCSRF
01

The five classic holes

Leaked secrets

A key in a NEXT_PUBLIC_ variable ends up in the client bundle, readable by anyone.

Unprotected routes

An API route with no session check: anyone calls the endpoint directly.

IDOR

You check the user is logged in, but not that the resource is theirs. They change the id and read someone else’s data.

XSS

dangerouslySetInnerHTML on unsanitized user content = script injection.

02

Keeping secrets server-side

In Next.js, the client/server boundary is easy to cross by accident. The reflex: mark sensitive modules server-only, and remember that anything NEXT_PUBLIC_ is public by definition.

lib/db.tsTSCopy
// lib/db.ts — empeche ce module de finir dans le bundle client
import "server-only";
// une variable SANS prefixe NEXT_PUBLIC_ n'est jamais exposee au navigateur
const dbUrl = process.env.DATABASE_URL; // OK, cote serveur uniquement
// NEXT_PUBLIC_API_KEY -> visible dans le bundle : ne JAMAIS y mettre un secret
Worth noting

Where to store and inject secrets properly is the whole of handling secrets properly.

03

Authenticate AND authorize

The most frequent and most serious mistake: checking who the user is, but not what they’re allowed to touch. Every route that reads or modifies a resource must verify the resource belongs to the user.

route.tsTSCopy
// app/api/invoices/[id]/route.ts
export async function GET(req, { params }) {
const session = await auth();
if (!session) return new Response("Unauthorized", { status: 401 });
const invoice = await db.invoice.findUnique({ where: { id: params.id } });
// IDOR : sans ce check, un user lit les factures d'un autre en changeant l'id
if (invoice.orgId !== session.orgId) {
return new Response("Forbidden", { status: 403 });
}
return Response.json(invoice);
}
Careful

Never trust an id coming from the client. Always verify ownership server-side — it’s the number-one SaaS vulnerability.

04

The rest of the minimum

·validate every input with a schema (Zod) before touching it — never blind trust;
·session cookies as httpOnly + Secure + SameSite to block theft and CSRF;
·security headers (CSP, HSTS) via next.config or middleware;
·rate limiting on sensitive routes (login, payment) to block brute-force.
Worth noting

Two of these have their own article: authentication in a Next.js app and rate limiting and API protection.

05

Sources & further reading

Sources & further reading
01Next.js — How to think about data security in Next.jsThe official guide: Data Access Layer, server-only, taint, and the “re-authorize inside every Server Action” rule.02Next.js — Environment variables (NEXT_PUBLIC_)States plainly that any NEXT_PUBLIC_ variable is inlined into the JS bundle at build time.03Next.js — Content Security PolicyHow to set a CSP with a nonce via middleware, or statically in next.config.04OWASP Top 10 — A01: Broken Access ControlThe number-one Top 10 category: it covers exactly the IDOR described above.05OWASP — IDOR Prevention Cheat SheetCheck ownership on every access, from the session — never from a client-supplied id.06OWASP — Cross Site Scripting Prevention Cheat SheetNames dangerouslySetInnerHTML without sanitization as a React XSS vector.07React — dangerouslySetInnerHTML (common components)React’s own docs: only use it on trusted, already-sanitized HTML.
In short

Basic security isn't an expert topic: it's a checklist. Server-side secrets, every route authorizing as much as authenticating, validated inputs, hardened cookies. Make it a systematic pass before every release, and you eliminate 90% of real vulnerabilities.

OWASP Top 10 ↗
Read next