Command Palette

Search for a command to run...

Security

Complete guide to security best practices for Next.js applications deployed on Vercel.

Security is fundamental in modern web applications. This guide provides practical examples and proven patterns to protect your Next.js application deployed on Vercel, based on official documentation and industry best practices.

CSP is essential to protect against XSS attacks, clickjacking, and code injection. Implement CSP with nonces in Next.js for enhanced security.

import { NextResponse } from 'next/server';export function proxy(request) {  // Generate unique nonce for each request  const nonce = Buffer.from(crypto.randomUUID()).toString('base64');  const isDev = process.env.NODE_ENV === 'development';  const cspHeader = `    default-src 'self';    script-src 'self' 'nonce-${nonce}' 'strict-dynamic' ${isDev ? "'unsafe-eval'" : ''};    style-src 'self' ${isDev ? "'unsafe-inline'" : `'nonce-${nonce}'`};    img-src 'self' blob: data: https:;    font-src 'self';    object-src 'none';    base-uri 'self';    form-action 'self';    frame-ancestors 'none';    upgrade-insecure-requests;  `;  const contentSecurityPolicyHeaderValue = cspHeader    .replace(/\s{2,}/g, ' ')    .trim();  const requestHeaders = new Headers(request.headers);  requestHeaders.set('x-nonce', nonce);  requestHeaders.set('Content-Security-Policy', contentSecurityPolicyHeaderValue);  const response = NextResponse.next({    request: { headers: requestHeaders },  });    response.headers.set('Content-Security-Policy', contentSecurityPolicyHeaderValue);  return response;}export const config = {  matcher: [    {      source: '/((?!api|_next/static|_next/image|favicon.ico).*)',      missing: [        { type: 'header', key: 'next-router-prefetch' },        { type: 'header', key: 'purpose', value: 'prefetch' },      ],    },  ],};

Handle secrets and credentials securely with environment variables. Vercel offers encrypted sensitive variables that are never exposed.

# Database credentials - NEVER commit to gitDATABASE_URL="postgresql://user:password@localhost:5432/mydb"# API Keys - Use Vercel's sensitive environment variablesSTRIPE_SECRET_KEY="sk_live_..."OPENAI_API_KEY="sk-..."# Public variables (can be exposed to browser)NEXT_PUBLIC_API_URL="https://api.example.com"# Auth secrets - Generate with: openssl rand -base64 32NEXTAUTH_SECRET="your-secret-here"NEXTAUTH_URL="https://yourdomain.com"# Vercel automatically provides these:# VERCEL_URL - Deployment URL# VERCEL_ENV - Environment (production, preview, development)

Protect routes and APIs with Proxy (formerly Middleware in Next.js 15). Verify authentication, authorization, and add security headers on every request.

import { NextResponse } from 'next/server';import { getToken } from 'next-auth/jwt';export async function proxy(request) {  const token = await getToken({     req: request,    secret: process.env.NEXTAUTH_SECRET   });  // Protect API routes  if (request.nextUrl.pathname.startsWith('/api/protected')) {    if (!token) {      return NextResponse.json(        { error: 'Unauthorized' },        { status: 401 }      );    }  }  // Protect admin routes  if (request.nextUrl.pathname.startsWith('/admin')) {    if (!token || token.role !== 'admin') {      return NextResponse.redirect(new URL('/login', request.url));    }  }  // Add security headers  const response = NextResponse.next();  response.headers.set('X-Frame-Options', 'DENY');  response.headers.set('X-Content-Type-Options', 'nosniff');  response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');  response.headers.set('Permissions-Policy', 'geolocation=(), microphone=(), camera=()');  return response;}export const config = {  matcher: ['/api/protected/:path*', '/admin/:path*'],};

Validate and sanitize all user inputs. Use validation schemas and sanitize HTML to prevent XSS attacks.

import { NextResponse } from 'next/server';import DOMPurify from 'isomorphic-dompurify';import { z } from 'zod';// Validation schemaconst PostSchema = z.object({  title: z.string().min(1).max(200),  content: z.string().min(1).max(10000),  authorId: z.string().uuid(),});export async function POST(request) {  try {    const body = await request.json();        // Validate input    const validatedData = PostSchema.parse(body);        // Sanitize HTML content to prevent XSS    const sanitizedContent = DOMPurify.sanitize(validatedData.content, {      ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'a'],      ALLOWED_ATTR: ['href'],    });        // Save to database with parameterized queries    const post = await db.post.create({      data: {        title: validatedData.title,        content: sanitizedContent,        authorId: validatedData.authorId,      },    });        return NextResponse.json(post, { status: 201 });  } catch (error) {    if (error instanceof z.ZodError) {      return NextResponse.json(        { error: 'Invalid input', details: error.errors },        { status: 400 }      );    }        // Don't expose internal errors    console.error('Post creation error:', error);    return NextResponse.json(      { error: 'Internal server error' },      { status: 500 }    );  }}

Protect your APIs against abuse and DDoS attacks by implementing rate limiting. Limit the number of requests per IP or user within a time period.

import { LRUCache } from 'lru-cache';// Rate limiter with LRU cacheexport function rateLimit(options = {}) {  const tokenCache = new LRUCache({    max: options.uniqueTokenPerInterval || 500,    ttl: options.interval || 60000,  });  return {    check: (limit, token) =>      new Promise((resolve, reject) => {        const tokenCount = tokenCache.get(token) || [0];                if (tokenCount[0] === 0) {          tokenCache.set(token, [1]);        }                tokenCount[0] += 1;        const currentUsage = tokenCount[0];        const isRateLimited = currentUsage >= limit;                return isRateLimited ? reject() : resolve();      }),  };}// Usage in API routeimport { NextResponse } from 'next/server';const limiter = rateLimit({  interval: 60 * 1000, // 1 minute  uniqueTokenPerInterval: 500,});export async function GET(request) {  const ip = request.headers.get('x-forwarded-for') || 'anonymous';    try {    await limiter.check(10, ip); // 10 requests per minute        // Your API logic here    return NextResponse.json({ data: 'success' });  } catch {    return NextResponse.json(      { error: 'Rate limit exceeded' },      { status: 429 }    );  }}

Vercel provides multiple security layers: Firewall, DDoS protection, encrypted sensitive variables, and automatic HTTPS on all deployments.

# Install Vercel CLInpm i -g vercel# Set sensitive environment variables via CLIvercel env add DATABASE_URL# Select environment: Production, Preview, Development# Type: sensitive (encrypted and non-readable)# Configure Vercel Firewall (Enterprise)# Enable firewall protection against common attacks:# - SQL Injection# - Cross-Site Scripting (XSS)# - Scanner Detection# - Local File Inclusion (LFI)# View deployment logs for security monitoringvercel logs <deployment-url># Enable Attack Challenge Mode# Go to: Project Settings > Security > Attack Challenge Mode

Protect against Cross-Site Request Forgery attacks using CSRF tokens. Verify the authenticity of each request that modifies data.

import { NextResponse } from 'next/server';import { randomBytes } from 'crypto';// Generate CSRF tokenexport async function GET() {  const token = randomBytes(32).toString('hex');    const response = NextResponse.json({ csrfToken: token });    // Set CSRF token in httpOnly cookie  response.cookies.set('csrf-token', token, {    httpOnly: true,    secure: process.env.NODE_ENV === 'production',    sameSite: 'strict',    maxAge: 3600, // 1 hour  });    return response;}

Keep your dependencies updated and free from vulnerabilities. Use npm audit, Snyk, or Dependabot to detect and fix issues.

{  "name": "secure-nextjs-app",  "version": "1.0.0",  "scripts": {    "dev": "next dev",    "build": "next build",    "start": "next start",    "audit": "npm audit",    "audit:fix": "npm audit fix",    "check-updates": "npx npm-check-updates"  },  "dependencies": {    "next": "^16.0.0",    "react": "^19.0.0",    "react-dom": "^19.0.0"  },  "devDependencies": {    "@types/node": "^20.0.0",    "@types/react": "^19.0.0",    "typescript": "^5.0.0"  }}

Configure cookies with security flags. Use httpOnly, secure, and sameSite to protect against XSS and CSRF.

import { NextResponse } from 'next/server';export async function POST(request) {  const response = NextResponse.json({ success: true });    // Secure cookie configuration  response.cookies.set('session', 'your-session-token', {    httpOnly: true,        // Prevents JavaScript access    secure: true,          // Only sent over HTTPS    sameSite: 'strict',    // CSRF protection    maxAge: 60 * 60 * 24,  // 1 day    path: '/',  });    return response;}

Use ORMs like Prisma or parameterized queries to prevent SQL injection. Never concatenate strings directly in queries.

import { NextResponse } from 'next/server';import { PrismaClient } from '@prisma/client';const prisma = new PrismaClient();export async function GET(request) {  const { searchParams } = new URL(request.url);  const email = searchParams.get('email');    // ❌ BAD: Vulnerable to SQL injection  // const user = await prisma.$queryRawUnsafe(  //   `SELECT * FROM users WHERE email = '${email}'`  // );    // ✅ GOOD: Use Prisma's type-safe queries  const user = await prisma.user.findUnique({    where: { email: email },    select: {      id: true,      email: true,      name: true,      // Don't select sensitive fields like password    },  });    return NextResponse.json(user);}// ✅ GOOD: Using parameterized queries if neededexport async function POST(request) {  const { name, email } = await request.json();    // Parameterized query prevents SQL injection  const user = await prisma.$queryRaw`    SELECT * FROM users     WHERE name = ${name} AND email = ${email}  `;    return NextResponse.json(user);}

Configure security headers in next.config.js. Include HSTS, X-Frame-Options, X-Content-Type-Options, and Referrer-Policy.

/** @type {import('next').NextConfig} */const nextConfig = {  reactStrictMode: true,    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: 'strict-origin-when-cross-origin'          },          {            key: 'Permissions-Policy',            value: 'camera=(), microphone=(), geolocation=()'          }        ],      },    ];  },    images: {    remotePatterns: [      {        protocol: 'https',        hostname: 'trusted-cdn.com',      },    ],  },};module.exports = nextConfig;

Server Actions must validate authentication and authorization. Always validate inputs and verify user permissions.

'use server';import { revalidatePath } from 'next/cache';import { getSession } from '@/lib/session';import { z } from 'zod';const UpdateProfileSchema = z.object({  name: z.string().min(1).max(100),  bio: z.string().max(500).optional(),});export async function updateProfile(formData) {  // 1. Verify authentication  const session = await getSession();  if (!session?.userId) {    throw new Error('Unauthorized');  }    // 2. Validate input  const rawData = {    name: formData.get('name'),    bio: formData.get('bio'),  };    const validatedData = UpdateProfileSchema.parse(rawData);    // 3. Check authorization  const user = await db.user.findUnique({    where: { id: session.userId },  });    if (!user) {    throw new Error('User not found');  }    // 4. Perform action  await db.user.update({    where: { id: session.userId },    data: validatedData,  });    // 5. Revalidate cache  revalidatePath('/profile');    return { success: true };}

Disable source maps in production to prevent exposing your original source code. Source maps reveal your application's internal structure and logic, making it easier for attackers to find vulnerabilities.

/** @type {import('next').NextConfig} */const nextConfig = {  reactStrictMode: true,    // Disable source maps in production  productionBrowserSourceMaps: false,    // Additional security configurations  compiler: {    // Remove console logs in production    removeConsole: process.env.NODE_ENV === 'production' ? {      exclude: ['error', 'warn'],    } : false,  },    webpack: (config, { dev, isServer }) => {    if (!dev && !isServer) {      // Disable source maps for client-side production builds      config.devtool = false;    }        return config;  },};module.exports = nextConfig;

Fundamental Security Principles

1. Defense in Depth
Multiple security layers to protect the application

• Implement CSP, CSRF, and rate limiting

• Use multiple authentication methods

• Validate on both client and server

• Monitor logs and set up alerts

2. Least Privilege
Grant only the minimum permissions necessary

• Users only access what they need

• APIs and services with limited permissions

• Environment variables only where required

• Review permissions regularly

3. Input Validation
Never trust user data

• Validate and sanitize all inputs

• Use validation schemas (Zod, Yup)

• Implement HTML sanitization

• Prevent SQL injection and XSS

4. Keep it Updated
Keep everything updated and monitored

• Update dependencies regularly

• Use audit tools (npm audit, Snyk)

• Apply security patches quickly

• Monitor known vulnerabilities