Building a secure Next.js application requires defense at every layer â from how you handle authentication and cookies, to how you validate input and configure HTTP headers. This tutorial walks through the most important security concerns specific to Next.js, with practical, production-ready patterns for each.
Danger
đ 1. Introduction
Next.js blurs the line between client and server, which means security decisions apply differently depending on where your code runs. A value that's safe to expose in a Server Component can be catastrophic if leaked into a Client Component bundle.
đ§ą 2. Security Fundamentals
Most security failures trace back to a small set of broken principles. Keeping these in mind guides nearly every decision below.
- Never trust client inputâ validate and sanitize everything on the server, regardless of client-side checks.
- Least privilegeâ grant users and services only the access they strictly need.
- Defense in depthâ layer multiple protections so a single failure doesn't compromise the app.
- Fail securelyâ errors should default to denying access, not granting it.
đ 3. Authentication Security
Authentication confirms who a user is. In Next.js, this typically happens via a library like NextAuth.js (Auth.js), Clerk, or a custom token-based flow handled in Route Handlers and Server Actions.
Verifying a session in a Server Component
import { auth } from "@/lib/auth";
import { redirect } from "next/navigation";
export default async function DashboardPage() {
const session = await auth();
if (!session) {
redirect("/login");
}
return <div>Welcome, {session.user.name}</div>;
}Important
đ 4. Authorization Security
Authorization determines what an authenticated user is allowed to do. Every protected action should check permissions independently, rather than assuming a prior check was sufficient.
app/actions.ts
"use server";
export async function deletePost(postId: string) {
const session = await auth();
if (!session) throw new Error("Unauthorized");
const post = await db.post.findUnique({ where: { id: postId } });
if (post?.authorId !== session.user.id && session.user.role !== "admin") {
throw new Error("Forbidden: you do not own this post");
}
await db.post.delete({ where: { id: postId } });
}Warning
đĢ 5. Session Security
Sessions track an authenticated user across requests. Poor session management â predictable IDs, missing expiration, or no rotation â is a common attack vector.
- Use cryptographically random, high-entropy session identifiers.
- Set a reasonable session expiration and enforce re-authentication for sensitive actions.
- Rotate session tokens after login and after privilege changes (e.g. password reset).
- Invalidate sessions server-side on logout â don't rely solely on clearing a client cookie.
đĒ 6. Cookie Security
Cookies storing session tokens must be configured with strict flags to resist theft and tampering.
Setting a secure cookie
import { cookies } from "next/headers";
const cookieStore = await cookies();
cookieStore.set("session", token, {
httpOnly: true,
secure: true,
sameSite: "lax",
path: "/",
maxAge: 60 * 60 * 24 * 7, // 7 days
});| Flag | Purpose |
|---|---|
| httpOnly | Prevents JavaScript from reading the cookie, blocking XSS-based theft |
| secure | Ensures the cookie is only sent over HTTPS |
| sameSite | Restricts cross-site sending, mitigating CSRF |
đĄī¸ 7. CSRF Protection
CSRF tricks an authenticated user's browser into submitting an unwanted request. Server Actions include built-in protection, but custom Route Handlers need explicit safeguards.
Information
Manual origin check in a Route Handler
export async function POST(request: Request) {
const origin = request.headers.get("origin");
const allowedOrigin = process.env.NEXT_PUBLIC_APP_URL;
if (origin !== allowedOrigin) {
return new Response("Forbidden", { status: 403 });
}
// Handle the request
}Best Practice
âī¸ 8. XSS Prevention
XSS injects malicious scripts into a page, often to steal cookies or session tokens. React escapes content by default, but a few patterns reopen the risk.
Dangerous vs. safe rendering
// â Dangerous: bypasses React's automatic escaping
<div dangerouslySetInnerHTML={{ __html: userInput }} />
// â
Safe: React escapes this automatically
<div>{userInput}</div>Danger
đ 9. Content Security Policy (CSP)
A CSP header restricts which sources of scripts, styles, and other resources a browser is allowed to load, acting as a strong second layer of defense against XSS.
next.config.js â Content-Security-Policy header
const cspHeader = `
default-src 'self';
script-src 'self' 'nonce-${nonce}' 'strict-dynamic';
style-src 'self' 'unsafe-inline';
img-src 'self' blob: data:;
connect-src 'self';
frame-ancestors 'none';
`;
module.exports = {
async headers() {
return [
{
source: "/(.*)",
headers: [
{ key: "Content-Security-Policy", value: cspHeader.replace(/\n/g, "") },
],
},
];
},
};Tip
đ 10. SQL Injection Prevention
SQL injection occurs when untrusted input is concatenated directly into a query string. Always use parameterized queries or a trusted ORM.
Safe vs. unsafe query construction
// â Vulnerable: string concatenation
const user = await db.raw(`SELECT * FROM users WHERE email = '${email}'`);
// â
Safe: parameterized query
const user = await db.raw("SELECT * FROM users WHERE email = ?", [email]);
// â
Safer: ORM with built-in parameterization
const user = await prisma.user.findUnique({ where: { email } });Danger
đī¸ 11. NoSQL Injection Prevention
NoSQL databases like MongoDB are vulnerable to a different flavor of injection, where attacker-controlled objects (not strings) alter query logic.
Vulnerable NoSQL query
// â Vulnerable: if req.body.password is { "$ne": null },
// this bypasses the password check entirely
const user = await db.collection("users").findOne({
email: req.body.email,
password: req.body.password,
});Warning
â 12. Input Validation
Validate every piece of external input â form submissions, query params, headers, uploaded files â at the server boundary, using a schema validation library like zod.
app/actions.ts
"use server";
import { z } from "zod";
const CreateUserSchema = z.object({
email: z.string().email(),
age: z.number().int().min(13).max(120),
username: z.string().min(3).max(20).regex(/^[a-zA-Z0-9_]+$/),
});
export async function createUser(input: unknown) {
const result = CreateUserSchema.safeParse(input);
if (!result.success) {
return { success: false, errors: result.error.flatten() };
}
// result.data is now fully typed and validated
}Best Practice
đ¤ 13. Output Encoding
Encoding output ensures data is rendered as inert text rather than executable markup or code, depending on the context it's placed in.
- HTML context: React escapes text content automatically â avoid dangerouslySetInnerHTML.
- URL context: use encodeURIComponent() before inserting values into query strings.
- Attribute context: avoid injecting unsanitized values into href or src attributes.
Caution
đ 14. Environment Variable Security
Environment variables prefixed with NEXT_PUBLIC_ are bundled into client-side JavaScript and visible to anyone who views your site's source.
.env â correct vs. incorrect prefixing
# â
Safe to expose to the browser
NEXT_PUBLIC_API_URL=https://api.example.com
# â Never prefix secrets with NEXT_PUBLIC_
NEXT_PUBLIC_DATABASE_URL=postgres://user:pass@host/db # WRONG
DATABASE_URL=postgres://user:pass@host/db # Correct â server-onlyDanger
đī¸ 15. Secrets Management
API keys, database credentials, and signing secrets should never live in source control. Use your hosting platform's environment variable management or a dedicated secrets manager.
- Store secrets in your platform's dashboard (Vercel, AWS Secrets Manager, HashiCorp Vault) rather than in code.
- Use different secrets for development, staging, and production environments.
- Rotate secrets periodically, and immediately after any suspected leak.
- Add .env* files to .gitignore before the first commit.
Warning
đ 16. API Security
Every API surface â Route Handlers, Server Actions, and external integrations â needs consistent authentication, authorization, and validation.
đ 17. Route Handler Security
Route Handlers are standard HTTP endpoints and inherit all the classic web API risks â they need explicit checks that aren't automatically applied.
app/api/admin/users/route.ts
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/lib/auth";
export async function GET(request: NextRequest) {
const session = await auth();
if (!session || session.user.role !== "admin") {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
const users = await db.user.findMany({ select: { id: true, email: true } });
return NextResponse.json(users);
}Important
⥠18. Server Action Security
Server Actions are exposed as public endpoints under the hood, even though they're called like regular functions. Never assume a Server Action is only reachable from your own UI.
app/actions.ts
"use server";
export async function updateUserRole(userId: string, newRole: string) {
const session = await auth();
// Re-check authorization inside every Server Action â
// it can be called directly, bypassing any UI restrictions
if (session?.user.role !== "admin") {
throw new Error("Unauthorized");
}
const validRoles = ["user", "editor", "admin"];
if (!validRoles.includes(newRole)) {
throw new Error("Invalid role");
}
await db.user.update({ where: { id: userId }, data: { role: newRole } });
}Danger
đĻ 19. Middleware Security
Middleware runs before a request reaches a page or Route Handler, making it a natural place for authentication gating, header injection, and bot filtering.
middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(request: NextRequest) {
const token = request.cookies.get("session")?.value;
if (request.nextUrl.pathname.startsWith("/dashboard") && !token) {
return NextResponse.redirect(new URL("/login", request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ["/dashboard/:path*"],
};Caution
đ 20. File Upload Security
File uploads introduce risks including malicious file execution, storage exhaustion, and path traversal. Validate rigorously before accepting any file.
- Restrict accepted file types by checking the actual file signature, not just the extension or Content-Type header.
- Enforce a strict maximum file size on both client and server.
- Store uploads outside the web root, or in dedicated object storage like S3.
- Generate randomized filenames rather than trusting user-supplied names.
- Scan uploads for malware if the app accepts files from untrusted users.
Warning
đ 21. CORS
CORS controls which external origins are allowed to call your API from a browser. Overly permissive CORS exposes your endpoints to any website.
app/api/public/route.ts
import { NextResponse } from "next/server";
const allowedOrigins = ["https://trusted-partner.com"];
export async function GET(request: Request) {
const origin = request.headers.get("origin") ?? "";
const response = NextResponse.json({ data: "public info" });
if (allowedOrigins.includes(origin)) {
response.headers.set("Access-Control-Allow-Origin", origin);
}
return response;
}Danger
đ§ž 22. Security Headers
A small set of HTTP response headers meaningfully reduce common attack surfaces with minimal effort.
| Header | Protects Against |
|---|---|
| X-Frame-Options: DENY | Clickjacking via iframe embedding |
| X-Content-Type-Options: nosniff | MIME-type sniffing attacks |
| Referrer-Policy: strict-origin-when-cross-origin | Leaking full URLs to third-party sites |
| Strict-Transport-Security | Protocol downgrade and cookie-hijacking attacks |
| Permissions-Policy | Unwanted access to camera, microphone, geolocation |
next.config.js
module.exports = {
async headers() {
return [
{
source: "/(.*)",
headers: [
{ key: "X-Frame-Options", value: "DENY" },
{ key: "X-Content-Type-Options", value: "nosniff" },
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
{ key: "Strict-Transport-Security", value: "max-age=63072000; includeSubDomains; preload" },
],
},
];
},
};đ 23. HTTPS
HTTPS encrypts traffic between the browser and server, preventing eavesdropping and tampering. It is a baseline requirement, not an optional hardening step.
- Managed platforms like Vercel provision and renew TLS certificates automatically.
- Self-hosted deployments should use certbot or a reverse proxy with automatic renewal.
- Redirect all http:// traffic to https:// and enable HSTS.
Important
đ§ 24. Rate Limiting
Rate limiting throttles how many requests a client can make in a given window, protecting against brute-force login attempts, scraping, and denial-of-service abuse.
Rate limiting with Upstash Redis
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(10, "60 s"),
});
export async function POST(request: Request) {
const ip = request.headers.get("x-forwarded-for") ?? "unknown";
const { success } = await ratelimit.limit(ip);
if (!success) {
return new Response("Too many requests", { status: 429 });
}
// Handle the request
}Best Practice
đ¤ 25. Bot Protection
Automated bots probe for vulnerabilities, scrape content, and abuse forms at scale. Layer multiple defenses rather than relying on a single technique.
- Add CAPTCHA or an invisible challenge (like Cloudflare Turnstile) on public forms.
- Use honeypot fields â hidden inputs that only bots fill in â to silently reject spam submissions.
- Combine rate limiting with bot detection for login and signup endpoints.
- Monitor for unusual traffic patterns, such as identical requests at inhuman speed.
đ 26. Authentication Best Practices
- Hash passwords with a slow, salted algorithm like bcrypt or argon2â never store them in plain text.
- Offer and encourage MFA for sensitive accounts.
- Lock or delay accounts after repeated failed login attempts.
- Use generic error messages like "Invalid credentials" rather than revealing whether the email or password was wrong.
- Send email notifications for sensitive account changes like password resets and new device logins.
Tip
đĄ 27. Security Monitoring
Monitoring turns security from a one-time setup into an ongoing practice by surfacing suspicious activity in real time.
- Log authentication failures, permission denials, and rate-limit triggers with enough context to investigate.
- Integrate an error-tracking tool like Sentry to catch unexpected exceptions that could indicate an exploit attempt.
- Set up alerts for anomalies â spikes in 401/403 responses, unusual geographic access patterns, or repeated failed logins.
Note
đ 28. Common Vulnerabilities
| Vulnerability | Root Cause | Mitigation |
|---|---|---|
| Broken access control | Missing server-side authorization checks | Re-verify permissions in every action and handler |
| XSS | Unsanitized HTML rendering | Avoid dangerouslySetInnerHTML, add CSP |
| Secrets exposure | Misused NEXT_PUBLIC_ prefix | Audit env vars before every deploy |
| Injection attacks | Unparameterized queries | Use an ORM or parameterized queries exclusively |
| Insecure direct object references | Trusting client-supplied IDs without ownership checks | Verify the requester owns or can access the resource |
đ 29. Security Checklist
To Do
- All Server Actions and Route Handlers re-check authentication and authorization.
- No secrets are prefixed with NEXT_PUBLIC_.
- Cookies use httpOnly, secure, and sameSite flags.
- A Content Security Policy is configured and tested.
- All database queries use parameterization or an ORM.
- Rate limiting is applied to authentication and public form endpoints.
- Security headers (HSTS, X-Frame-Options, etc.) are set for all responses.
- File uploads validate type, size, and content before storage.
- Dependencies are audited regularly with npm audit.
â 30. Frequently Asked Questions
Question
Answer
Question
Answer
Question
Answer
đ 31. Summary
Security in Next.js is a shared responsibilityacross authentication, authorization, input handling, and infrastructure configuration. No single control is sufficient on its own â the strongest applications layer multiple defenses so that one failure doesn't become a breach.
Summary
- Re-check authentication and authorization on the server for every Server Action and Route Handler.
- Never expose secrets through the NEXT_PUBLIC_ prefix.
- Validate and sanitize all external input; encode all output appropriately for its context.
- Configure security headers, CSP, and HTTPS as non-negotiable baselines.
- Rate limit sensitive endpoints and monitor for suspicious activity continuously.