Next.js Security Best Practices: Protecting Your Application
Learn essential security practices for Next.js applications including authentication, CSRF protection, XSS prevention, and secure API routes.

Next.js Security Best Practices: Protecting Your Application
Security is paramount in modern web applications. Next.js provides powerful features for building secure applications, but understanding how to properly implement security measures is crucial. In this article, we'll explore essential security practices for Next.js applications, covering authentication, CSRF protection, XSS prevention, and secure API routes.
Understanding Next.js Security Model
Next.js applications run in multiple environments:
- Server-side: API routes, Server Components, Server Actions
- Client-side: Client Components, browser JavaScript
- Build-time: Static generation, optimization
Each environment requires different security considerations.
Authentication and Authorization
1. Secure Session Management
Use httpOnly cookies for session tokens to prevent XSS attacks:
// app/api/auth/login/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { cookies } from 'next/headers';
export async function POST(request: NextRequest) {
const { email, password } = await request.json();
// Validate credentials
const user = await authenticateUser(email, password);
if (!user) {
return NextResponse.json(
{ error: 'Invalid credentials' },
{ status: 401 }
);
}
// Generate secure token
const token = await generateToken(user);
// Set httpOnly cookie
cookies().set('session_token', token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 60 * 60 * 24 * 7, // 7 days
path: '/',
});
return NextResponse.json({ success: true });
}2. Middleware for Route Protection
Use Next.js middleware to protect routes:
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const token = request.cookies.get('session_token')?.value;
// Protect dashboard routes
if (request.nextUrl.pathname.startsWith('/dashboard')) {
if (!token) {
return NextResponse.redirect(new URL('/login', request.url));
}
// Verify token validity
try {
verifyToken(token);
} catch (error) {
return NextResponse.redirect(new URL('/login', request.url));
}
}
return NextResponse.next();
}
export const config = {
matcher: ['/dashboard/:path*'],
};3. Server Actions Authentication
Protect Server Actions with authentication checks:
// app/actions/user-actions.ts
'use server';
import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';
async function getAuthenticatedUser() {
const token = cookies().get('session_token')?.value;
if (!token) {
redirect('/login');
}
try {
const user = await verifyToken(token);
return user;
} catch (error) {
redirect('/login');
}
}
export async function updateProfile(formData: FormData) {
const user = await getAuthenticatedUser();
// Only allow users to update their own profile
const userId = formData.get('userId');
if (userId !== user.id) {
throw new Error('Unauthorized');
}
// Update profile logic
await updateUserProfile(userId, formData);
}CSRF Protection
1. CSRF Tokens for Forms
Implement CSRF protection for form submissions:
// app/api/csrf/route.ts
import { NextResponse } from 'next/server';
import { cookies } from 'next/headers';
import crypto from 'crypto';
export async function GET() {
const token = crypto.randomBytes(32).toString('hex');
cookies().set('csrf_token', token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 60 * 60, // 1 hour
});
return NextResponse.json({ csrfToken: token });
}2. Validate CSRF Tokens
Validate CSRF tokens in API routes:
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { cookies } from 'next/headers';
export async function POST(request: NextRequest) {
const body = await request.json();
const csrfToken = request.headers.get('X-CSRF-Token');
const cookieToken = cookies().get('csrf_token')?.value;
if (!csrfToken || csrfToken !== cookieToken) {
return NextResponse.json(
{ error: 'Invalid CSRF token' },
{ status: 403 }
);
}
// Process request
return NextResponse.json({ success: true });
}XSS Prevention
1. Sanitize User Input
Always sanitize user input before rendering:
// lib/sanitize.ts
import DOMPurify from 'isomorphic-dompurify';
export function sanitizeHtml(dirty: string): string {
return DOMPurify.sanitize(dirty, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
ALLOWED_ATTR: ['href'],
});
}2. Use React's Built-in Escaping
React automatically escapes content, but be careful with dangerouslySetInnerHTML:
// app/components/UserContent.tsx
'use client';
import { sanitizeHtml } from '@/lib/sanitize';
export function UserContent({ content }: { content: string }) {
// ✅ Good: Sanitize before rendering
const sanitized = sanitizeHtml(content);
return (
<div dangerouslySetInnerHTML={{ __html: sanitized }} />
);
}3. Content Security Policy
Implement CSP headers:
// next.config.js
module.exports = {
async headers() {
return [
{
source: '/:path*',
headers: [
{
key: 'Content-Security-Policy',
value: [
"default-src 'self'",
"script-src 'self' 'unsafe-eval' 'unsafe-inline'",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
"font-src 'self'",
"connect-src 'self' https://api.example.com",
].join('; '),
},
],
},
];
},
};Secure API Routes
1. Input Validation
Always validate and sanitize input:
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
const createUserSchema = z.object({
email: z.string().email(),
name: z.string().min(1).max(100),
age: z.number().int().min(18).max(120),
});
export async function POST(request: NextRequest) {
try {
const body = await request.json();
// Validate input
const validatedData = createUserSchema.parse(body);
// Process request
const user = await createUser(validatedData);
return NextResponse.json(user, { status: 201 });
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json(
{ error: 'Invalid input', details: error.errors },
{ status: 400 }
);
}
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}2. Rate Limiting
Implement rate limiting to prevent abuse:
// lib/rate-limit.ts
import { LRUCache } from 'lru-cache';
const rateLimit = new LRUCache<string, number>({
max: 500,
ttl: 60 * 1000, // 1 minute
});
export function rateLimitCheck(identifier: string, limit: number = 10): boolean {
const count = rateLimit.get(identifier) || 0;
if (count >= limit) {
return false;
}
rateLimit.set(identifier, count + 1);
return true;
}// app/api/auth/login/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { rateLimitCheck } from '@/lib/rate-limit';
export async function POST(request: NextRequest) {
const ip = request.ip || request.headers.get('x-forwarded-for') || 'unknown';
if (!rateLimitCheck(`login:${ip}`, 5)) {
return NextResponse.json(
{ error: 'Too many requests' },
{ status: 429 }
);
}
// Process login
}3. SQL Injection Prevention
Use parameterized queries (via ORM):
// ✅ Good: Using Prisma (parameterized)
const user = await prisma.user.findUnique({
where: { email: userEmail },
});
// ❌ Bad: Raw SQL without parameters
const user = await prisma.$queryRaw`
SELECT * FROM users WHERE email = ${userEmail}
`;Environment Variables Security
1. Server-Only Variables
Never expose sensitive variables to the client:
// ✅ Good: Server-only variable
// .env.local
DATABASE_URL=postgresql://...
API_SECRET_KEY=secret123
// app/api/users/route.ts
const apiKey = process.env.API_SECRET_KEY; // ✅ Server-only// ❌ Bad: Exposing secrets to client
// next.config.js
module.exports = {
env: {
API_SECRET_KEY: process.env.API_SECRET_KEY, // ❌ Exposed to client!
},
};2. Client-Safe Variables
Use NEXT_PUBLIC_ prefix only for client-safe variables:
// ✅ Good: Public API URL
// .env.local
NEXT_PUBLIC_API_URL=https://api.example.com
// Client component
const apiUrl = process.env.NEXT_PUBLIC_API_URL;Secure Headers
1. Security Headers Configuration
Configure security headers in next.config.js:
// next.config.js
module.exports = {
async headers() {
return [
{
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',
},
{
key: 'X-Content-Type-Options',
value: 'nosniff',
},
{
key: 'X-XSS-Protection',
value: '1; mode=block',
},
{
key: 'Referrer-Policy',
value: 'origin-when-cross-origin',
},
],
},
];
},
};File Upload Security
1. Validate File Types
Always validate file types and sizes:
// app/api/upload/route.ts
import { NextRequest, NextResponse } from 'next/server';
const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/webp'];
const MAX_SIZE = 5 * 1024 * 1024; // 5MB
export async function POST(request: NextRequest) {
const formData = await request.formData();
const file = formData.get('file') as File;
if (!file) {
return NextResponse.json(
{ error: 'No file provided' },
{ status: 400 }
);
}
// Validate file type
if (!ALLOWED_TYPES.includes(file.type)) {
return NextResponse.json(
{ error: 'Invalid file type' },
{ status: 400 }
);
}
// Validate file size
if (file.size > MAX_SIZE) {
return NextResponse.json(
{ error: 'File too large' },
{ status: 400 }
);
}
// Process file
const buffer = await file.arrayBuffer();
// Store securely...
}Best Practices Summary
- Always validate input on both client and server
- Use httpOnly cookies for session tokens
- Implement CSRF protection for state-changing operations
- Sanitize user-generated content before rendering
- Use parameterized queries to prevent SQL injection
- Implement rate limiting to prevent abuse
- Never expose secrets to the client
- Use HTTPS in production
- Keep dependencies updated to patch security vulnerabilities
- Regular security audits of your application
Conclusion
Security in Next.js requires attention to multiple layers: authentication, input validation, XSS prevention, CSRF protection, and secure API design. By following these practices, you can build robust, secure Next.js applications that protect both your users and your data. Remember that security is an ongoing process—regular audits and updates are essential.
References
Want more insights?
Subscribe to our newsletter or follow us for more updates on software development and team scaling.
Contact Us