A practical security checklist for Next.js apps covering headers, CSP, API routes, authentication, dependency security, and more.
Anay Pandya
Founder of ZeriFlow · 10 years fullstack engineering · About the author
Key Takeaways
Security headers are your first line of defense. Next.js makes them easy to configure in next.config.js:
<div class="zf-stat-callout" style="background:#0d1117;border:1px solid rgba(16,185,129,0.25);border-left:3px solid #10b981;border-radius:4px;padding:16px 20px;margin:24px 0"> <p style="margin:0 0 4px;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:0.15em;color:#10b981;font-family:monospace">ZeriFlow Data — 12,400+ sites analyzed</p> <p style="margin:0;font-size:13px;color:#e2e8f0;line-height:1.6;font-family:monospace">In our analysis of 12,400+ sites scanned on ZeriFlow, 64% lack a Content-Security-Policy header — and of those that have one, 71% use 'unsafe-inline', negating XSS protection entirely.</p> </div>
Is your site actually secure?
Run a free check — 60 seconds
Keep reading
Anay Pandya
1,428 words
/** @type {import('next').NextConfig} */
const nextConfig = {
async headers() {
return [
{
source: '/(.*)',
headers: [
{ key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'X-Frame-Options', value: 'DENY' },
{ key: 'X-XSS-Protection', value: '0' },
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
{ key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' },
{
key: 'Strict-Transport-Security',
value: 'max-age=31536000; includeSubDomains; preload',
},
],
},
];
},
};
module.exports = nextConfig;Checklist:
Note: Do NOT set X-Powered-By. Next.js sends it by default — disable it:
const nextConfig = {
poweredByHeader: false,
// ... headers config above
};CSP is the most powerful security header and the most complex to configure. In Next.js, you have two approaches:
{
key: 'Content-Security-Policy',
value: "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self' https://your-api.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self';"
}// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const nonce = Buffer.from(crypto.randomUUID()).toString('base64');
const cspHeader = `
default-src 'self';
script-src 'self' 'nonce-${nonce}' 'strict-dynamic';
style-src 'self' 'nonce-${nonce}';
img-src 'self' blob: data:;
font-src 'self';
connect-src 'self';
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
object-src 'none';
`.replace(/\n/g, '');
const response = NextResponse.next();
response.headers.set('Content-Security-Policy', cspHeader);
response.headers.set('x-nonce', nonce);
return response;
}Then pass the nonce to your scripts in your layout:
// app/layout.tsx
import { headers } from 'next/headers';
export default async function RootLayout({ children }) {
const headersList = await headers();
const nonce = headersList.get('x-nonce') ?? '';
return (
<html lang="en">
<body>
<Script nonce={nonce} src="/analytics.js" />
{children}
</body>
</html>
);
}Checklist:
Next.js API routes (and Server Actions in App Router) are server endpoints that need the same protections as any backend API.
// lib/rate-limit.ts
const rateLimitMap = new Map<string, { count: number; lastReset: number }>();
export function rateLimit(ip: string, limit = 10, windowMs = 60000): boolean {
const now = Date.now();
const entry = rateLimitMap.get(ip);
if (!entry || now - entry.lastReset > windowMs) {
rateLimitMap.set(ip, { count: 1, lastReset: now });
return true;
}
if (entry.count >= limit) return false;
entry.count++;
return true;
}For production, use a Redis-backed solution like @upstash/ratelimit:
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') ?? '127.0.0.1';
const { success } = await ratelimit.limit(ip);
if (!success) {
return Response.json({ error: 'Too many requests' }, { status: 429 });
}
// ... handle request
}Never trust client input. Validate everything:
import { z } from 'zod';
const ContactSchema = z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
message: z.string().min(10).max(5000),
});
export async function POST(request: Request) {
const body = await request.json();
const result = ContactSchema.safeParse(body);
if (!result.success) {
return Response.json({ error: 'Invalid input' }, { status: 400 });
}
// Use result.data — it is typed and validated
}Checklist:
Next.js has a specific pattern for environment variables:
NEXT_PUBLIC_* — exposed to the browser (use only for non-sensitive values)# .env.local (never commit this file)
DATABASE_URL=postgresql://...
STRIPE_SECRET_KEY=sk_live_...
JWT_SECRET=your-256-bit-secret
# Safe to expose to browser
NEXT_PUBLIC_API_URL=https://api.yoursite.com
NEXT_PUBLIC_SITE_URL=https://yoursite.comChecklist:
process.env.SECRET in API routes onlyIf you use NextAuth.js (Auth.js), next-auth, or Clerk:
// middleware.ts — protect routes
import { withAuth } from 'next-auth/middleware';
export default withAuth({
pages: {
signIn: '/login',
},
});
export const config = {
matcher: ['/dashboard/:path*', '/api/protected/:path*'],
};Key practices:
Secure, HttpOnly, SameSite=LaxChecklist:
React automatically escapes JSX output, which prevents most XSS. But there are exceptions:
// DANGEROUS — never do this with user input
<div dangerouslySetInnerHTML={{ __html: userInput }} />
// SAFE — React escapes this
<div>{userInput}</div>If you must render HTML (e.g., from a CMS), sanitize it:
import DOMPurify from 'isomorphic-dompurify';
const clean = DOMPurify.sanitize(dirtyHtml);
<div dangerouslySetInnerHTML={{ __html: clean }} />Checklist:
dangerouslySetInnerHTMLYour node_modules folder likely contains more code than you wrote yourself. Keep it secure:
# Check for known vulnerabilities
npm audit
# Fix automatically where possible
npm audit fix
# For a detailed report
npm audit --jsonChecklist:
For API routes that serve external clients:
// app/api/public/route.ts
export async function GET(request: Request) {
const data = { message: 'Public data' };
return Response.json(data, {
headers: {
'Access-Control-Allow-Origin': 'https://yourfrontend.com',
'Access-Control-Allow-Methods': 'GET, POST',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
});
}Never use Access-Control-Allow-Origin: * on routes that use cookies or authentication.
After implementing this checklist, verify your work with an automated scan.
Checklist:
ZeriFlow checks your security headers, SSL/TLS, cookies, and more in 60 seconds. It catches the misconfigurations that are easy to miss during development — like a CSP that looks correct but has a permissive fallback, or an HSTS header with a max-age that is too short.
| # | Check | Priority |
|---|---|---|
| 1-6 | HTTP Security Headers | Critical |
| 7-9 | Content Security Policy | Critical |
| 10-12 | API Route Security | Critical |
| 13-15 | Environment Variables | Critical |
| 16 | Authentication | Critical |
| 17 | XSS Prevention | High |
| 18-19 | Dependency Security | High |
| 20 | Automated Scan | High |
Tackle the critical items before launch. Address the high-priority items within the first week. Then set up regular scanning to catch regressions.
<!-- zf-internal-links -->
Scan your Vercel deployment's security headers.
80+ checks in 60 seconds — free.
Related resources
Website Vulnerability Scanner
Run a broader website security audit across headers, TLS, DNS, cookies, SEO, and disclosure checks.
Security Headers Checker
Check CSP, HSTS, X-Frame-Options, and other response headers.
SSL Checker
Review TLS certificate, HTTPS, and transport security signals.
DMARC Checker
Validate email authentication records for domain spoofing protection.
CSP Checker
Review Content-Security-Policy coverage and common gaps.
October 10, 2025
SME Security Starter Pack: 12 Controls That Actually Move the Needle
Monitor your security score
Schedule recurring scans and catch regressions after deployments.