Skip to main content
Juliano Alves
Back to blog

Security headers in Next.js (App Router)

5 min read
By Juliano Alves

HTTP security headers are cheap wins: they reduce clickjacking, MIME sniffing, downgrade attacks, and some classes of XSS—especially when combined with a thoughtful Content-Security-Policy (CSP). Next.js lets you attach headers globally via next.config and refine them per route in middleware.ts.

This post covers what I enable by default, how I roll out CSP without breaking third-party scripts, and what headers cannot replace.

Baseline headers in next.config#

/** @type {import('next').NextConfig} */
const securityHeaders = [
  { key: 'X-DNS-Prefetch-Control', value: 'on' },
  { 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=()',
  },
];

const nextConfig = {
  async headers() {
    return [
      {
        source: '/:path*',
        headers: securityHeaders,
      },
    ];
  },
};

module.exports = nextConfig;

What each does#

  • X-Frame-Options: DENY — prevents embedding your site in <iframe> (clickjacking). Use SAMEORIGIN only if you intentionally iframe yourself.
  • X-Content-Type-Options: nosniff — stops browsers from MIME-sniffing responses away from declared Content-Type.
  • Referrer-Policy — limits referrer leakage on cross-origin navigations.
  • Permissions-Policy — disables powerful features by default; tighten per route if you need camera/mic.

Strict-Transport-Security (HSTS)#

Only emit HSTS when all production traffic is HTTPS and you will not revert to HTTP:

Strict-Transport-Security: max-age=63072000; includeSubDomains; preload

preload is optional and commits you to the browser preload list—read the requirements before enabling.

Content-Security-Policy: staged rollout#

CSP is powerful and fragile: one wrong directive blocks analytics, payment widgets, or inline styles.

Step 1: Report-Only#

Content-Security-Policy-Report-Only: default-src 'self'; report-uri https://csp.example.com/report

Collect violations in production without enforcing. Tune directives until noise drops.

Step 2: Nonces for inline scripts (App Router)#

For first-party inline scripts, prefer nonces over 'unsafe-inline'. Next.js 13+ documents patterns using headers() in middleware or experimental CSP helpers—pick the approach that matches your hosting (Vercel vs self-hosted).

Step 3: Enforce#

Flip to Content-Security-Policy once dashboards show green. Keep a fast rollback path (feature flag or config toggle).

Middleware for dynamic headers#

When headers depend on the user, tenant, or experiment, middleware.ts can append or override:

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

export function middleware(req: NextRequest) {
  const res = NextResponse.next();
  res.headers.set('X-Robots-Tag', req.nextUrl.pathname.startsWith('/draft') ? 'noindex' : 'all');
  return res;
}

Keep middleware fast: no remote calls unless absolutely necessary—they run on every matched request.

What headers do not fix#

  • Authentication and authorization bugs.
  • SQL injection or command injection from unsanitized input.
  • Broken access control in your API.

Headers are defense in depth, not the primary control.

Testing#

  • Add security header checks to E2E or Playwright global setup (basic presence assertions).
  • Use securityheaders.com or Mozilla Observatory for a second opinion.

Summary#

Start with a small, strict baseline in next.config, add HSTS when HTTPS is guaranteed, and treat CSP as a staged project with Report-Only and nonces—not a one-line copy-paste. Use middleware when policies vary by route or tenant.

© 2026 Juliano Alves. All rights reserved.