Skip to main content
Ganesh Joshi
Back to Blogs

Security headers in Next.js: CSP, HSTS, and more

February 15, 20264 min read
Tips
Headers or config on screen

Security headers protect your web application from common attacks. They tell browsers how to handle your content, what sources to trust, and what features to allow. Next.js doesn't set most security headers by default—you need to configure them.

Why security headers matter

Attack Header that helps
XSS (Cross-Site Scripting) Content-Security-Policy
Clickjacking X-Frame-Options
MIME sniffing X-Content-Type-Options
Man-in-the-middle Strict-Transport-Security
Information leakage Referrer-Policy

Without headers, browsers use permissive defaults that attackers exploit.

Setting headers in next.config.js

Use the headers function for static headers:

// next.config.js
module.exports = {
  async headers() {
    return [
      {
        source: '/(.*)',
        headers: [
          {
            key: 'X-Content-Type-Options',
            value: 'nosniff',
          },
          {
            key: 'X-Frame-Options',
            value: 'DENY',
          },
          {
            key: 'X-XSS-Protection',
            value: '1; mode=block',
          },
          {
            key: 'Referrer-Policy',
            value: 'strict-origin-when-cross-origin',
          },
          {
            key: 'Permissions-Policy',
            value: 'camera=(), microphone=(), geolocation=()',
          },
        ],
      },
    ];
  },
};

Strict-Transport-Security (HSTS)

HSTS forces HTTPS for your domain:

{
  key: 'Strict-Transport-Security',
  value: 'max-age=31536000; includeSubDomains; preload',
}
Directive Purpose
max-age Seconds to remember HTTPS-only
includeSubDomains Apply to all subdomains
preload Eligible for browser preload lists

Only enable HSTS when your entire site works over HTTPS. Once set, browsers will refuse HTTP connections.

HSTS preload

Submit your domain to hstspreload.org for inclusion in browser preload lists. Browsers will enforce HTTPS before the first visit.

Content-Security-Policy

CSP is the most complex but most protective header. It controls what resources can load:

{
  key: 'Content-Security-Policy',
  value: [
    "default-src 'self'",
    "script-src 'self' 'unsafe-inline' https://trusted-cdn.com",
    "style-src 'self' 'unsafe-inline'",
    "img-src 'self' data: https:",
    "font-src 'self'",
    "connect-src 'self' https://api.example.com",
    "frame-ancestors 'none'",
    "base-uri 'self'",
    "form-action 'self'",
  ].join('; '),
}

CSP directives

Directive Controls
default-src Fallback for other directives
script-src JavaScript sources
style-src CSS sources
img-src Image sources
font-src Font sources
connect-src XHR, fetch, WebSocket
frame-ancestors Who can embed your page
base-uri Base URL restrictions
form-action Form submission targets

CSP source values

Value Meaning
'self' Same origin
'none' Nothing allowed
'unsafe-inline' Inline scripts/styles (avoid for scripts)
'unsafe-eval' eval() and similar (avoid)
https: Any HTTPS source
data: Data URIs
'nonce-abc123' Specific nonce
'sha256-...' Specific hash

Report-Only mode

Test CSP without breaking your site:

{
  key: 'Content-Security-Policy-Report-Only',
  value: "default-src 'self'; report-uri /api/csp-report",
}

The browser reports violations but doesn't block them. Review reports before enforcing.

Dynamic CSP with middleware

For CSP with nonces (needed for strict inline script policies):

// 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 csp = [
    `default-src 'self'`,
    `script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`,
    `style-src 'self' 'unsafe-inline'`,
    `img-src 'self' data: https:`,
    `font-src 'self'`,
    `connect-src 'self'`,
    `frame-ancestors 'none'`,
  ].join('; ');

  const response = NextResponse.next();
  response.headers.set('Content-Security-Policy', csp);
  response.headers.set('x-nonce', nonce); // Pass to components

  return response;
}

Use the nonce in your layout:

// app/layout.tsx
import { headers } from 'next/headers';

export default async function RootLayout({ children }) {
  const nonce = headers().get('x-nonce');

  return (
    <html>
      <head>
        <script nonce={nonce} src="/analytics.js" />
      </head>
      <body>{children}</body>
    </html>
  );
}

X-Frame-Options

Prevents clickjacking by controlling iframe embedding:

{
  key: 'X-Frame-Options',
  value: 'DENY', // or 'SAMEORIGIN'
}
Value Meaning
DENY Never allow embedding
SAMEORIGIN Only same-origin embedding

CSP's frame-ancestors is more flexible but X-Frame-Options has wider support.

Permissions-Policy

Disable browser features you don't use:

{
  key: 'Permissions-Policy',
  value: [
    'camera=()',
    'microphone=()',
    'geolocation=()',
    'interest-cohort=()', // Disable FLoC
  ].join(', '),
}

Referrer-Policy

Control referrer information sent with requests:

{
  key: 'Referrer-Policy',
  value: 'strict-origin-when-cross-origin',
}
Value Behavior
no-referrer Never send referrer
strict-origin-when-cross-origin Full path for same-origin, origin only for cross-origin
same-origin Only for same-origin requests

Complete configuration

// next.config.js
const securityHeaders = [
  {
    key: 'X-Content-Type-Options',
    value: 'nosniff',
  },
  {
    key: 'X-Frame-Options',
    value: 'DENY',
  },
  {
    key: 'X-XSS-Protection',
    value: '1; mode=block',
  },
  {
    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',
  },
];

module.exports = {
  async headers() {
    return [
      {
        source: '/(.*)',
        headers: securityHeaders,
      },
    ];
  },
};

Testing headers

  1. SecurityHeaders.com: Scan your deployed site for a grade
  2. DevTools: Network tab > select request > Headers
  3. curl: curl -I https://yoursite.com
  4. Observatory: Mozilla's observatory.mozilla.org

Summary

Security headers protect against XSS, clickjacking, and other attacks. Configure them in next.config.js for static headers or middleware.ts for dynamic values like CSP nonces. Start with Report-Only mode for CSP to identify issues. Test with securityheaders.com and aim for an A grade. Enable HSTS only when fully HTTPS. These headers add significant protection with minimal performance cost.

Frequently Asked Questions

Essential headers include Content-Security-Policy, Strict-Transport-Security, X-Content-Type-Options, X-Frame-Options, and Referrer-Policy. These protect against XSS, clickjacking, and other attacks.

Use the headers function in next.config.js for static headers. For dynamic headers like CSP with nonces, use middleware.ts to modify response headers on each request.

CSP tells browsers which sources can load scripts, styles, images, and other resources. A strict CSP prevents XSS attacks by blocking unauthorized code execution.

Avoid 'unsafe-inline' for scripts when possible—it defeats much of CSP's XSS protection. Use nonces or hashes for inline scripts. For styles, 'unsafe-inline' is sometimes needed with CSS-in-JS.

Use securityheaders.com to scan your site and get a grade. Check browser DevTools Network tab to verify headers are present. Start with Report-Only mode for CSP before enforcing.

Related Posts