Skip to main content
Back to blog
March 7, 2026|10 min read|Hardening Guides

Next.js Security Checklist: 20 Things You Must Do Before Launch

A practical security checklist for Next.js apps covering headers, CSP, API routes, authentication, dependency security, and more.

ZeriFlow Team

1,331 words

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:

javascript
/** @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:

javascript
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)

javascript
{
  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';"
}
typescript
// 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:

typescript
// 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

typescript
// 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:

typescript
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:

typescript
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.com

Checklist:

  • [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 accessprocess.env.SECRET in API routes only

Authentication Best Practices

If you use NextAuth.js (Auth.js), next-auth, or Clerk:

typescript
// 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:

typescript
// 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:

typescript
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:

bash
# Check for known vulnerabilities
npm audit

# Fix automatically where possible
npm audit fix

# For a detailed report
npm audit --json

Checklist:

  • [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:

typescript
// 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

#CheckPriority
1-6HTTP Security HeadersCritical
7-9Content Security PolicyCritical
10-12API Route SecurityCritical
13-15Environment VariablesCritical
16AuthenticationCritical
17XSS PreventionHigh
18-19Dependency SecurityHigh
20Automated ScanHigh

Tackle the critical items before launch. Address the high-priority items within the first week. Then set up regular scanning to catch regressions.

Ready to check your site?

Run a free security scan in 30 seconds.

Related articles

Keep reading