HTTP Security Headers in Next.js
Security headers are your first line of defense. Next.js makes them easy to configure in next.config.js:
/** @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:
- [x] 1. X-Content-Type-Options: nosniff — prevents MIME-type sniffing
- [x] 2. X-Frame-Options: DENY — prevents clickjacking
- [x] 3. Referrer-Policy — controls information leakage in referrer headers
- [x] 4. Permissions-Policy — restricts browser API access
- [x] 5. Strict-Transport-Security — enforces HTTPS
- [x] 6. X-XSS-Protection: 0 — disable the legacy XSS auditor (it can be exploited; rely on CSP instead)
Note: Do NOT set X-Powered-By. Next.js sends it by default — disable it:
const nextConfig = {
poweredByHeader: false,
// ... headers config above
};Content Security Policy for Next.js
CSP is the most powerful security header and the most complex to configure. In Next.js, you have two approaches:
Static CSP (simpler)
{
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';"
}Nonce-based CSP (recommended for App Router)
// 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:
- [x] 7. CSP is configured — either static or nonce-based
- [x] 8. No `unsafe-eval` — never allow eval() in your CSP
- [x] 9. `frame-ancestors 'none'` — replaces X-Frame-Options in CSP
API Route Security
Next.js API routes (and Server Actions in App Router) are server endpoints that need the same protections as any backend API.
Rate limiting
// 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
}Input validation with Zod
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:
- [x] 10. Rate limiting on all API routes — especially auth and form endpoints
- [x] 11. Input validation with a schema library — Zod, Yup, or similar
- [x] 12. Authentication checks on protected routes — verify JWT/session on every request
Environment Variables & Secrets
Next.js has a specific pattern for environment variables:
NEXT_PUBLIC_*— exposed to the browser (use only for non-sensitive values)- All other variables — server-only
# .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:
- [x] 13. No secrets in `NEXT_PUBLIC_` variables — these are embedded in the client bundle
- [x] 14. `.env.local` is in `.gitignore` — verify right now
- [x] 15. Server-only secrets use server-side access —
process.env.SECRETin API routes only
Authentication Best Practices
If 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:
- Always validate the session server-side, never rely on client-side state alone
- Set secure cookie flags:
Secure,HttpOnly,SameSite=Lax - Implement CSRF protection (NextAuth does this by default)
- Use short session expiry with refresh tokens
Checklist:
- [x] 16. Session validation is server-side — not just client checks
Input Validation & XSS Prevention
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:
- [x] 17. No unescaped user input — audit all uses of
dangerouslySetInnerHTML
Dependency Security
Your 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:
- [x] 18. Run `npm audit` regularly — integrate into your CI pipeline
- [x] 19. Keep dependencies updated — use Dependabot or Renovate for automated PRs
CORS Configuration
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.
Run Your Automated Security Scan
After implementing this checklist, verify your work with an automated scan.
Checklist:
- [x] 20. Run a full security scan — Use zeriflow.com to test your Next.js app
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.
Summary table
| # | 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.
