Security of Next.js applications is an important aspect that cannot be ignored in production environments. Comprehensive security protection measures can protect applications from various network attacks and data breaches.
Core Security Concepts
1. Security Threat Types
- XSS (Cross-Site Scripting): Malicious script injection
- CSRF (Cross-Site Request Forgery): Forged user requests
- SQL Injection: Malicious SQL code execution
- SSRF (Server-Side Request Forgery): Forged server requests
- Clickjacking: UI spoofing attacks
- Man-in-the-Middle: Communication interception
Security Configuration and Protection
1. Environment Variable Management
javascript// .env.local (development environment) DATABASE_URL=postgresql://localhost:5432/myapp NEXTAUTH_SECRET=dev-secret-key-change-in-production NEXTAUTH_URL=http://localhost:3000 API_KEY=dev-api-key // .env.production (production environment) DATABASE_URL=postgresql://prod-db-server:5432/myapp NEXTAUTH_SECRET=${NEXTAUTH_SECRET} # Get from deployment platform NEXTAUTH_URL=https://myapp.com API_KEY=${API_KEY} # Get from deployment platform // .env.example (template file) DATABASE_URL=postgresql://localhost:5432/myapp NEXTAUTH_SECRET=your-secret-key-here NEXTAUTH_URL=http://localhost:3000 API_KEY=your-api-key-here // .gitignore .env.local .env.production .env.development *.env // Environment variable validation // lib/env.js import { z } from 'zod'; const envSchema = z.object({ DATABASE_URL: z.string().url(), NEXTAUTH_SECRET: z.string().min(32), NEXTAUTH_URL: z.string().url(), API_KEY: z.string().min(16), NODE_ENV: z.enum(['development', 'production', 'test']), }); export const env = envSchema.parse(process.env); // Using environment variables // lib/db.js import { env } from './env'; export const db = createConnection(env.DATABASE_URL);
2. CSP (Content Security Policy) Configuration
javascript// next.config.js const ContentSecurityPolicy = require('./lib/csp'); module.exports = { async headers() { return [ { source: '/(.*)', headers: [ { key: 'Content-Security-Policy', value: ContentSecurityPolicy.toString(), }, { key: 'X-Frame-Options', value: 'DENY', }, { key: 'X-Content-Type-Options', value: 'nosniff', }, { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin', }, { key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()', }, ], }, ]; }, }; // lib/csp.js const ContentSecurityPolicy = ` default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline' https://cdn.example.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: https: blob:; font-src 'self' https://fonts.gstatic.com; connect-src 'self' https://api.example.com; media-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; upgrade-insecure-requests; `; export default ContentSecurityPolicy; // Dynamic CSP policy // lib/csp.js import { NextResponse } from 'next/server'; export function getCSPHeaders() { const nonce = Buffer.from(crypto.randomUUID()).toString('base64'); return { 'Content-Security-Policy': ` default-src 'self'; script-src 'self' 'nonce-${nonce}' https://cdn.example.com; style-src 'self' 'nonce-${nonce}' https://fonts.googleapis.com; img-src 'self' data: https: blob:; `.replace(/\s{2,}/g, ' ').trim(), 'X-Nonce': nonce, }; } // Using nonce in pages // app/layout.js export default function RootLayout({ children }) { const nonce = headers().get('X-Nonce'); return ( <html lang="en"> <head> <script nonce={nonce} src="https://cdn.example.com/analytics.js" /> </head> <body>{children}</body> </html> ); }
3. XSS Protection
javascript// Using React's automatic escaping // app/page.js export default function Page({ userContent }) { // React automatically escapes, preventing XSS return <div>{userContent}</div>; } // Dangerous operation: using dangerouslySetInnerHTML export default function Page({ userContent }) { // If necessary, sanitize content first const sanitizedContent = DOMPurify.sanitize(userContent); return ( <div dangerouslySetInnerHTML={{ __html: sanitizedContent }} /> ); } // Using DOMPurify to clean HTML // lib/sanitize.js import DOMPurify from 'isomorphic-dompurify'; export function sanitizeHTML(html) { return DOMPurify.sanitize(html, { ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'ul', 'ol', 'li'], ALLOWED_ATTR: [], }); } export function sanitizeURL(url) { try { const parsed = new URL(url); if (!['http:', 'https:'].includes(parsed.protocol)) { return ''; } return parsed.href; } catch { return ''; } } // Input validation in API routes // app/api/comments/route.js import { z } from 'zod'; import { sanitizeHTML } from '@/lib/sanitize'; const commentSchema = z.object({ content: z.string().min(1).max(1000), userId: z.string().uuid(), }); export async function POST(request) { const body = await request.json(); // Validate input const validatedData = commentSchema.parse(body); // Sanitize HTML content const sanitizedContent = sanitizeHTML(validatedData.content); // Save to database const comment = await saveComment({ ...validatedData, content: sanitizedContent, }); return Response.json(comment); }
4. CSRF Protection
javascript// Using next-safe-action to prevent CSRF // app/actions.js 'use server'; import { action, makeSafeActionClient } from 'next-safe-action'; import { z } from 'zod'; const safeActionClient = makeSafeActionClient(); export const createComment = action( z.object({ content: z.string().min(1).max(1000), postId: z.string().uuid(), }), async ({ content, postId }) => { // Automatically validates CSRF token const comment = await createCommentInDB({ content, postId }); return { success: true, comment }; } ); // Custom CSRF protection // lib/csrf.js import { cookies } from 'next/headers'; const CSRF_SECRET = process.env.CSRF_SECRET; export async function generateCSRFToken() { const token = crypto.randomUUID(); const cookieStore = await cookies(); cookieStore.set('csrf_token', token, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'strict', maxAge: 60 * 60, // 1 hour }); return token; } export async function validateCSRFToken(token) { const cookieStore = await cookies(); const storedToken = cookieStore.get('csrf_token'); if (!storedToken || storedToken.value !== token) { throw new Error('Invalid CSRF token'); } return true; } // Using CSRF protection in API routes // app/api/comments/route.js import { validateCSRFToken } from '@/lib/csrf'; export async function POST(request) { const body = await request.json(); const csrfToken = request.headers.get('X-CSRF-Token'); // Validate CSRF token await validateCSRFToken(csrfToken); // Process request const comment = await createComment(body); return Response.json(comment); } // Client-side request includes CSRF token // components/CommentForm.js 'use client'; import { useState, useEffect } from 'react'; export default function CommentForm({ postId }) { const [content, setContent] = useState(''); const [csrfToken, setCsrfToken] = useState(''); useEffect(() => { // Get CSRF token fetch('/api/csrf-token') .then(res => res.json()) .then(data => setCsrfToken(data.token)); }, []); const handleSubmit = async (e) => { e.preventDefault(); await fetch('/api/comments', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken, }, body: JSON.stringify({ content, postId }), }); }; return ( <form onSubmit={handleSubmit}> <textarea value={content} onChange={(e) => setContent(e.target.value)} /> <button type="submit">Submit</button> </form> ); }
5. Authentication and Authorization
javascript// Using NextAuth.js for authentication // app/api/auth/[...nextauth]/route.js import NextAuth from 'next-auth'; import CredentialsProvider from 'next-auth/providers/credentials'; import GoogleProvider from 'next-auth/providers/google'; import { verifyPassword } from '@/lib/auth'; export const { handlers, auth, signIn, signOut } = NextAuth({ providers: [ GoogleProvider({ clientId: process.env.GOOGLE_CLIENT_ID, clientSecret: process.env.GOOGLE_CLIENT_SECRET, }), CredentialsProvider({ name: 'Credentials', credentials: { email: { label: 'Email', type: 'email' }, password: { label: 'Password', type: 'password' }, }, async authorize(credentials) { const user = await getUserByEmail(credentials.email); if (!user) { throw new Error('User not found'); } const isValidPassword = await verifyPassword( credentials.password, user.password ); if (!isValidPassword) { throw new Error('Invalid password'); } return { id: user.id, email: user.email, name: user.name, role: user.role, }; }, }), ], callbacks: { async jwt({ token, user }) { if (user) { token.id = user.id; token.role = user.role; } return token; }, async session({ session, token }) { session.user.id = token.id; session.user.role = token.role; return session; }, }, pages: { signIn: '/login', error: '/auth/error', }, session: { strategy: 'jwt', maxAge: 30 * 24 * 60 * 60, // 30 days }, secret: process.env.NEXTAUTH_SECRET, }); export { handlers as GET, handlers as POST }; // Role-Based Access Control (RBAC) // lib/auth.js import { auth } from '@/app/api/auth/[...nextauth]/config'; export const ROLES = { ADMIN: 'admin', USER: 'user', GUEST: 'guest', }; export async function requireAuth() { const session = await auth(); if (!session) { throw new Error('Unauthorized'); } return session; } export async function requireRole(role) { const session = await requireAuth(); if (session.user.role !== role) { throw new Error('Forbidden'); } return session; } export async function requireAnyRole(...roles) { const session = await requireAuth(); if (!roles.includes(session.user.role)) { throw new Error('Forbidden'); } return session; } // Using authorization in API routes // app/api/admin/users/route.js import { requireRole, ROLES } from '@/lib/auth'; export async function GET() { const session = await requireRole(ROLES.ADMIN); const users = await getAllUsers(); return Response.json(users); } // Using authorization in pages // app/admin/page.js import { redirect } from 'next/navigation'; import { requireRole, ROLES } from '@/lib/auth'; export default async function AdminPage() { const session = await requireRole(ROLES.ADMIN); return ( <div> <h1>Admin Dashboard</h1> <p>Welcome, {session.user.name}</p> </div> ); }
6. Database Security
javascript// Using parameterized queries to prevent SQL injection // lib/db.js import { Pool } from 'pg'; const pool = new Pool({ connectionString: process.env.DATABASE_URL, }); export async function getUserById(id) { const result = await pool.query( 'SELECT * FROM users WHERE id = $1', [id] ); return result.rows[0]; } export async function createUser(data) { const result = await pool.query( 'INSERT INTO users (email, password, name) VALUES ($1, $2, $3) RETURNING *', [data.email, data.password, data.name] ); return result.rows[0]; } // Using ORM to prevent SQL injection // lib/prisma.js import { PrismaClient } from '@prisma/client'; const prisma = new PrismaClient(); export async function getUserById(id) { return prisma.user.findUnique({ where: { id }, }); } export async function createUser(data) { return prisma.user.create({ data: { email: data.email, password: data.password, name: data.name, }, }); } // Database connection security // lib/db.js import { Pool } from 'pg'; const pool = new Pool({ connectionString: process.env.DATABASE_URL, ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false, } : false, max: 20, // Maximum connections idleTimeoutMillis: 30000, connectionTimeoutMillis: 2000, }); // Database migration security // prisma/migrate.ts import { PrismaClient } from '@prisma/client'; const prisma = new PrismaClient(); async function main() { // Use transactions to ensure data consistency await prisma.$transaction(async (tx) => { await tx.user.create({ data: { email: 'admin@example.com', password: await hashPassword('admin123'), role: 'admin', }, }); }); } main() .catch((e) => { console.error(e); process.exit(1); }) .finally(async () => { await prisma.$disconnect(); });
7. API Security
javascript// Rate limiting // lib/rate-limit.js import { Ratelimit } from '@upstash/ratelimit'; import { Redis } from '@upstash/redis'; const ratelimit = new Ratelimit({ redis: Redis.fromEnv(), limiter: Ratelimit.slidingWindow(10, '10 s'), analytics: true, }); export async function rateLimit(identifier) { const { success, limit, remaining, reset } = await ratelimit.limit(identifier); if (!success) { throw new Error('Rate limit exceeded'); } return { limit, remaining, reset }; } // Using rate limiting in API routes // app/api/comments/route.js import { rateLimit } from '@/lib/rate-limit'; import { headers } from 'next/headers'; export async function POST(request) { const ip = headers().get('x-forwarded-for') || 'unknown'; // Apply rate limiting await rateLimit(ip); const body = await request.json(); const comment = await createComment(body); return Response.json(comment); } // API key validation // lib/api-key.js export async function validateApiKey(apiKey) { const validKeys = process.env.API_KEYS?.split(',') || []; if (!validKeys.includes(apiKey)) { throw new Error('Invalid API key'); } return true; } // Validating API key in API routes // app/api/data/route.js import { validateApiKey } from '@/lib/api-key'; import { headers } from 'next/headers'; export async function GET(request) { const apiKey = headers().get('x-api-key'); // Validate API key await validateApiKey(apiKey); const data = await fetchData(); return Response.json(data); } // Input validation // lib/validation.js import { z } from 'zod'; export const commentSchema = z.object({ content: z.string().min(1).max(1000), postId: z.string().uuid(), userId: z.string().uuid(), }); export const userSchema = z.object({ email: z.string().email(), password: z.string().min(8).regex(/[A-Z]/).regex(/[0-9]/), name: z.string().min(2).max(100), }); // Using validation in API routes // app/api/users/route.js import { userSchema } from '@/lib/validation'; export async function POST(request) { const body = await request.json(); // Validate input const validatedData = userSchema.parse(body); // Process request const user = await createUser(validatedData); return Response.json(user); }
8. File Upload Security
javascript// File upload validation // lib/file-upload.js import { z } from 'zod'; const ALLOWED_MIME_TYPES = [ 'image/jpeg', 'image/png', 'image/gif', 'image/webp', ]; const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB export const fileSchema = z.object({ name: z.string().max(255), type: z.enum(ALLOWED_MIME_TYPES), size: z.number().max(MAX_FILE_SIZE), }); export async function validateFile(file) { // Validate file type if (!ALLOWED_MIME_TYPES.includes(file.type)) { throw new Error('Invalid file type'); } // Validate file size if (file.size > MAX_FILE_SIZE) { throw new Error('File too large'); } // Validate file content const buffer = await file.arrayBuffer(); const uint8Array = new Uint8Array(buffer); // Check file header (magic numbers) if (file.type === 'image/jpeg') { if (uint8Array[0] !== 0xFF || uint8Array[1] !== 0xD8) { throw new Error('Invalid JPEG file'); } } return true; } // Secure file upload API // app/api/upload/route.js import { validateFile } from '@/lib/file-upload'; import { uploadToS3 } from '@/lib/s3'; export async function POST(request) { const formData = await request.formData(); const file = formData.get('file'); if (!file) { return Response.json({ error: 'No file provided' }, { status: 400 }); } try { // Validate file await validateFile(file); // Generate safe filename const ext = file.name.split('.').pop(); const safeName = `${crypto.randomUUID()}.${ext}`; // Upload to S3 const url = await uploadToS3(file, safeName); return Response.json({ url }); } catch (error) { return Response.json({ error: error.message }, { status: 400 }); } }
9. Logging and Monitoring
javascript// Security logging // lib/logger.js import pino from 'pino'; const logger = pino({ level: process.env.LOG_LEVEL || 'info', transport: process.env.NODE_ENV === 'development' ? { target: 'pino-pretty', options: { colorize: true, }, } : undefined, }); export function logSecurityEvent(event, data) { logger.warn({ type: 'SECURITY_EVENT', event, timestamp: new Date().toISOString(), ...data, }); } export function logError(error, context = {}) { logger.error({ type: 'ERROR', error: error.message, stack: error.stack, timestamp: new Date().toISOString(), ...context, }); } // Usage example // app/api/auth/login/route.js import { logSecurityEvent } from '@/lib/logger'; export async function POST(request) { const body = await request.json(); try { const user = await authenticateUser(body.email, body.password); return Response.json({ user }); } catch (error) { // Log security event logSecurityEvent('LOGIN_FAILED', { email: body.email, ip: request.headers.get('x-forwarded-for'), userAgent: request.headers.get('user-agent'), }); return Response.json({ error: 'Invalid credentials' }, { status: 401 }); } }
Security Best Practices
- Environment Variable Security: Use .env.local, don't commit to version control
- CSP Configuration: Strict content security policy
- Input Validation: All user input must be validated
- Output Encoding: Prevent XSS attacks
- CSRF Protection: Use token validation
- Authentication Authorization: Implement appropriate authentication and authorization mechanisms
- Rate Limiting: Prevent brute force attacks
- HTTPS: Enforce HTTPS usage
- Dependency Updates: Regularly update dependency packages
- Security Audits: Regularly conduct security audits and penetration testing
By implementing these security measures, you can significantly improve the security of Next.js applications.