Web Security

Next.js Security Checklist: 15 Must-Do Hardening Steps

A practical Next.js security checklist covering CSP headers, server action auth, env var exposure, middleware guards, and SSRF prevention.

March 9, 20266 min readShipSafer Team

Next.js Security Checklist: 15 Must-Do Hardening Steps

Next.js ships with sensible defaults, but defaults are not the same as secure. The App Router, Server Actions, and edge middleware introduce new attack surfaces that many teams miss entirely. This checklist covers the 15 most impactful hardening steps you can apply to any Next.js application today.

1. Set Security Headers in next.config

The fastest win. Add a headers() function to your next.config.ts to inject security headers on every response:

// next.config.ts
const nextConfig = {
  async headers() {
    return [
      {
        source: '/(.*)',
        headers: [
          { key: 'X-Frame-Options', value: 'DENY' },
          { key: 'X-Content-Type-Options', value: 'nosniff' },
          { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
          { key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' },
          {
            key: 'Strict-Transport-Security',
            value: 'max-age=63072000; includeSubDomains; preload',
          },
        ],
      },
    ];
  },
};

2. Configure a Content Security Policy

CSP is the primary defense against XSS. In Next.js, nonce-based CSP is the recommended approach because it works with server-rendered inline scripts:

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { randomBytes } from 'crypto';

export function middleware(request: NextRequest) {
  const nonce = randomBytes(16).toString('base64');
  const csp = `
    default-src 'self';
    script-src 'self' 'nonce-${nonce}' 'strict-dynamic';
    style-src 'self' 'nonce-${nonce}';
    img-src 'self' blob: data:;
    connect-src 'self';
    frame-ancestors 'none';
  `.replace(/\s{2,}/g, ' ').trim();

  const response = NextResponse.next();
  response.headers.set('Content-Security-Policy', csp);
  response.headers.set('x-nonce', nonce);
  return response;
}

3. Authenticate Every Server Action

Server Actions are HTTP endpoints. Without explicit auth checks, any user — authenticated or not — can call them directly. Never rely on the UI to gate access:

'use server';

import { getAuthenticatedUser } from '@/lib/auth-middleware';

export async function deleteRecord(id: string) {
  const user = await getAuthenticatedUser();
  if (!user) {
    return { success: false, error: 'Authentication required' };
  }
  // proceed
}

4. Validate Server Action Input with Zod

Server Actions receive raw POST data. Validate it before touching the database:

'use server';

import { z } from 'zod';

const schema = z.object({
  email: z.string().email(),
  role: z.enum(['viewer', 'editor']),
});

export async function inviteUser(data: unknown) {
  const parsed = schema.safeParse(data);
  if (!parsed.success) {
    return { success: false, error: 'Invalid input' };
  }
  // parsed.data is now type-safe
}

5. Never Expose Server-Only Env Vars to the Client

Any variable prefixed with NEXT_PUBLIC_ is bundled into the client JavaScript. Do not prefix secrets:

# .env.local
DATABASE_URL=mongodb+srv://...       # server-only, never exposed
JWT_SECRET=supersecret               # server-only, never exposed
NEXT_PUBLIC_POSTHOG_KEY=phc_...      # safe to expose

Audit your bundle with ANALYZE=true next build and the @next/bundle-analyzer package to catch accidental leaks.

6. Protect Routes with Middleware Auth Guards

Use middleware.ts to redirect unauthenticated users before the page renders:

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

const PROTECTED = ['/dashboard', '/settings', '/api/private'];

export function middleware(request: NextRequest) {
  const isProtected = PROTECTED.some((path) =>
    request.nextUrl.pathname.startsWith(path)
  );

  if (isProtected) {
    const token = request.cookies.get('authToken');
    if (!token) {
      return NextResponse.redirect(new URL('/login', request.url));
    }
  }

  return NextResponse.next();
}

export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
};

7. Prevent SSRF via Server Actions

Server Actions run on the server and can make internal HTTP requests. If you accept a URL from user input and fetch it, you are vulnerable to Server-Side Request Forgery:

'use server';

import { URL } from 'url';

const ALLOWED_HOSTS = new Set(['api.example.com', 'cdn.example.com']);

export async function fetchExternalData(urlString: string) {
  let url: URL;
  try {
    url = new URL(urlString);
  } catch {
    return { success: false, error: 'Invalid URL' };
  }

  if (!ALLOWED_HOSTS.has(url.hostname)) {
    return { success: false, error: 'Host not allowed' };
  }

  const response = await fetch(url.toString());
  return { success: true, data: await response.json() };
}

Never allow requests to 169.254.169.254 (AWS IMDS), localhost, or private RFC-1918 ranges.

8. Use HTTP-Only Cookies for Session Tokens

Do not store JWTs in localStorage. Store them in HTTP-only cookies so JavaScript cannot read them:

import { cookies } from 'next/headers';

export function setAuthCookie(token: string) {
  cookies().set({
    name: 'authToken',
    value: token,
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax',
    maxAge: 60 * 60 * 24 * 7,
    path: '/',
  });
}

9. Add CSRF Protection for Mutations

Next.js Server Actions include built-in CSRF protection via origin checking, but custom API routes (/api/*) do not. Add explicit CSRF tokens or verify the Origin header:

// app/api/webhook/route.ts
export async function POST(request: Request) {
  const origin = request.headers.get('origin');
  const allowedOrigin = process.env.APP_URL;

  if (origin !== allowedOrigin) {
    return new Response('Forbidden', { status: 403 });
  }
  // handle request
}

10. Sanitize HTML Before Rendering

If you must render user-supplied HTML, sanitize it with DOMPurify:

import DOMPurify from 'isomorphic-dompurify';

// Safe
const clean = DOMPurify.sanitize(userHtml);
return <div dangerouslySetInnerHTML={{ __html: clean }} />;

Never pass raw user input to dangerouslySetInnerHTML.

11. Disable x-powered-by and Server Headers

Next.js removes X-Powered-By by default, but confirm it stays off:

// next.config.ts
const nextConfig = {
  poweredByHeader: false,
};

12. Restrict API Routes to Authenticated Requests

Every API route handler should verify the session before processing:

// app/api/data/route.ts
import { getAuthenticatedUser } from '@/lib/auth-middleware';

export async function GET() {
  const user = await getAuthenticatedUser();
  if (!user) {
    return new Response('Unauthorized', { status: 401 });
  }
  // return data
}

13. Set cache: 'no-store' on Sensitive Fetch Calls

Server Components that fetch sensitive data can have responses cached by the CDN. Opt out:

const data = await fetch('https://api.example.com/user/profile', {
  cache: 'no-store',
  headers: { Authorization: `Bearer ${token}` },
});

14. Audit Third-Party Dependencies Regularly

npm audit
npx snyk test
npx better-npm-audit audit

Lock your dependency versions and run audits in CI. A compromised dependency has the same access as your own code.

15. Enable TypeScript Strict Mode

TypeScript strict mode catches null dereferences and type mismatches that could become runtime vulnerabilities:

// tsconfig.json
{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true
  }
}

Quick Reference

StepImpactEffort
Security headersHighLow
CSP nonceHighMedium
Server action authCriticalLow
Zod validationHighLow
HTTP-only cookiesHighLow
Middleware auth guardHighLow
SSRF URL allowlistHighLow
npm audit in CIMediumLow

Work through this list top-to-bottom. The first six items can be implemented in an afternoon and eliminate the most common attack vectors in Next.js applications.

Check Your Security Score — Free

See exactly how your domain scores on DMARC, TLS, HTTP headers, and 25+ other automated security checks in under 60 seconds.