Security is not a plugin you add at the end; it is an architectural requirement. In a modern Next.js application, security responsibilities are split between the Edge (Middleware), the Configuration (next.config.js), and the Server Runtime (Server Actions).
This guide details a cohesive security strategy using Next.js, Zod (validation), and Bcrypt (cryptography).
I. The Security Checklist
Before implementing code, ensure your infrastructure meets these baselines:
- [ ] SSL/TLS: Enforce HTTPS strictly (HSTS).
- [ ] Content Security Policy (CSP): Restrict data sources to trusted domains.
- [ ] Database Security: Never concatenate SQL; use ORMs (Prisma/Drizzle) or parameterized queries.
- [ ] Input Sanitization: Validate data types and length on the server.
-
[ ] Secure Dependencies: regularly run
npm audit.
II. The First Line of Defense: Headers & Config
In Next.js, security headers are injected at the build time configuration. This ensures that every response—static or dynamic—carries the necessary protection instructions for the browser.
File: next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
async headers() {
return [
{
// Apply these headers to all routes in your application
source: '/:path*',
headers: [
{
key: 'X-DNS-Prefetch-Control',
value: 'on',
},
{
key: 'Strict-Transport-Security',
value: 'max-age=63072000; includeSubDomains; preload',
},
{
key: 'X-Frame-Options',
value: 'SAMEORIGIN', // Prevents clickjacking
},
{
key: 'X-Content-Type-Options',
value: 'nosniff', // Prevents MIME-sniffing
},
{
key: 'Referrer-Policy',
value: 'strict-origin-when-cross-origin',
},
],
},
];
},
};
module.exports = nextConfig;
III. Authentication & Cryptography
Handling passwords requires strict adherence to one rule: never store them in plain text. In the Node.js/Next.js runtime, we use bcrypt (or bcryptjs) to handle one-way hashing.
Installation: npm install bcryptjs
1. The Hashing Utility
Create a dedicated utility file for crypto operations. This ensures you can rotate algorithms easily in the future.
File: lib/auth-utils.ts
import bcrypt from 'bcryptjs';
const SALT_ROUNDS = 12;
/**
* Hashes a plain text password.
* Usage: Before saving a new user to the DB.
*/
export async function hashPassword(password: string): Promise<string> {
return await bcrypt.hash(password, SALT_ROUNDS);
}
/**
* Verifies a plain text password against a stored hash.
* Usage: During the login process.
*/
export async function verifyPassword(
plainText: string,
hashed: string
): Promise<boolean> {
return await bcrypt.compare(plainText, hashed);
}
IV. Secure Input Handling (Server Actions)
In the Next.js App Router, Server Actions are publicly accessible HTTP endpoints. You must validate data structure and types before processing logic. In We use Zod for schema validation.
Installation: npm install zod
File: app/actions/auth-actions.ts
'use server'
import { z } from 'zod';
import { hashPassword } from '@/lib/auth-utils';
import { db } from '@/lib/db'; // Hypothetical DB connection
import { redirect } from 'next/navigation';
// 1. Define the Strict Schema
const SignUpSchema = z.object({
email: z.string().email({ message: "Invalid email format" }),
password: z.string()
.min(8, { message: "Password must be at least 8 characters" })
.regex(/[A-Z]/, { message: "Must contain one uppercase letter" })
.regex(/[0-9]/, { message: "Must contain one number" }),
role: z.enum(['user', 'admin']).default('user'), // Enforce allowed values
});
export async function registerUser(prevState: any, formData: FormData) {
// 2. Parse and Validate Input
const result = SignUpSchema.safeParse({
email: formData.get('email'),
password: formData.get('password'),
role: 'user', // Force role to user prevents Mass Assignment vulnerabilities
});
if (!result.success) {
return { error: result.error.flatten().fieldErrors };
}
const { email, password } = result.data;
// 3. Securely Hash Password
const hashedPassword = await hashPassword(password);
// 4. Database Interaction (Example with Prisma/Drizzle)
try {
await db.user.create({
data: {
email,
password: hashedPassword,
},
});
} catch (e) {
return { error: "User already exists" };
}
redirect('/login');
}
V. Session Management & Cookies
When managing sessions manually or via libraries, configuring the cookie attributes is vital to prevent Cross-Site Scripting (XSS) and Cross-Site Request Forgery (CSRF).
File: lib/session.ts
import { cookies } from 'next/headers';
export async function createSession(userId: string) {
// Set the expiration time (e.g., 7 days)
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
const sessionToken = "generated_jwt_or_session_id"; // Replace with actual token logic
const cookieStore = await cookies();
cookieStore.set('session', sessionToken, {
httpOnly: true, // CRITICAL: Prevents JavaScript access (stops XSS theft)
secure: process.env.NODE_ENV === 'production', // Only send over HTTPS
sameSite: 'lax', // Protects against CSRF while allowing normal navigation
expires: expiresAt,
path: '/',
});
}
VI. Output Sanitization (XSS Prevention)
Next.js automatically sanitizes data passed to JSX components. However, if you must render HTML stored in a database (e.g., a blog post body), you bypass this protection.
You must use a sanitizer library like isomorphic-dompurify before rendering raw HTML.
Installation: npm install isomorphic-dompurify
File: components/SafeHTML.tsx
import DOMPurify from 'isomorphic-dompurify';
interface Props {
htmlContent: string;
}
export default function SafeHTML({ htmlContent }: Props) {
// Cleanses the HTML of scripts, iframes, and event handlers (e.g., onclick)
const sanitizedHTML = DOMPurify.sanitize(htmlContent);
// Now safe to render
return (
<div
className="prose"
dangerouslySetInnerHTML={{ __html: sanitizedHTML }}
/>
);
}
Top comments (1)
This is a really thoughtful breakdown of full-stack security architecture. I love how you emphasize that security isn’t an afterthought but a core architectural concern — especially by layering defenses across the edge (middleware), config, and server runtime. Your use of Next.js with Zod for validation and bcrypt for hashing is concrete and practical. The checklist for SSL/TLS, CSP, secure dependencies, and input sanitization is super helpful for any dev team looking to build securely from day one. Thanks for putting this together — much more devs need to think in terms of defense-in-depth like this! 🔐