Skip to main content
Back to blog
June 27, 2025·Updated May 10, 2026|10 min read|Anay Pandya|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.

Anay Pandya

1,428 words

AP

Anay Pandya

Founder of ZeriFlow · 10 years fullstack engineering · About the author

Key Takeaways

  • A practical security checklist for Next.js apps covering headers, CSP, API routes, authentication, dependency security, and more.
  • Includes copy-paste code examples and step-by-step instructions.
  • Free automated scan available to verify your implementation.

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:

<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

Scan free →
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](https://zeriflow.com/blog/x-content-type-options-nosniff): 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.


Further Reading

<!-- zf-internal-links -->

Scan your Vercel deployment's security headers.

80+ checks in 60 seconds — free.

Related resources

Keep improving your website security

Run free scan

Related articles

Keep reading