All articles
Tutorials8 min readDecember 28, 2025
Next.jsSecurity HeadersCSPHTTPS

How to Add Security Headers to Your Next.js App: Complete Implementation

Step-by-step guide to implementing security headers in Next.js. Protect against XSS, clickjacking, and more.

Security Guide

Why Security Headers Matter

Security headers tell browsers how to behave when handling your site. Without them, you're vulnerable to:

  • Cross-site scripting (XSS)
  • Clickjacking
  • MIME type sniffing
  • Man-in-the-middle attacks

Quick Implementation

next.config.js

javascript
// next.config.js

const securityHeaders = [ { key: 'X-DNS-Prefetch-Control', value: 'on' }, { key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubDomains; preload' }, { key: 'X-XSS-Protection', value: '1; mode=block' }, { key: 'X-Frame-Options', value: 'SAMEORIGIN' }, { key: 'X-Content-Type-Options', value: 'nosniff' }, { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' }, { key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' } ]

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

Header-by-Header Explanation

Strict-Transport-Security (HSTS)

javascript
{
  key: 'Strict-Transport-Security',
  value: 'max-age=63072000; includeSubDomains; preload'
}

What it does: Forces HTTPS for 2 years, includes subdomains.

Why you need it: Prevents downgrade attacks and cookie hijacking.

X-Frame-Options

javascript
{
  key: 'X-Frame-Options',
  value: 'SAMEORIGIN'  // or 'DENY'
}

What it does: Prevents your site from being embedded in iframes.

Why you need it: Prevents clickjacking attacks.

Options:

  • DENY - Never allow framing
  • SAMEORIGIN - Allow framing from same origin only

X-Content-Type-Options

javascript
{
  key: 'X-Content-Type-Options',
  value: 'nosniff'
}

What it does: Prevents browser from MIME-sniffing files.

Why you need it: Stops execution of malicious files disguised as other types.

Referrer-Policy

javascript
{
  key: 'Referrer-Policy',
  value: 'strict-origin-when-cross-origin'
}

What it does: Controls how much referrer info is sent.

Why you need it: Prevents leaking sensitive URLs to external sites.

Options:

  • no-referrer - Never send
  • strict-origin - Send origin only, HTTPS only
  • strict-origin-when-cross-origin - Full URL same-origin, origin only cross-origin

Permissions-Policy

javascript
{
  key: 'Permissions-Policy',
  value: 'camera=(), microphone=(), geolocation=()'
}

What it does: Disables browser features you don't need.

Why you need it: Reduces attack surface from feature abuse.

Common restrictions:

javascript
value: 'camera=(), microphone=(), geolocation=(), interest-cohort=()'

Content Security Policy (CSP)

CSP is the most powerful (and complex) security header.

Basic CSP

javascript
{
  key: 'Content-Security-Policy',
  value: "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline';"
}

CSP for Next.js with Common Services

javascript
const ContentSecurityPolicy = 
  default-src 'self';
  script-src 'self' 'unsafe-eval' 'unsafe-inline' https://www.googletagmanager.com;
  style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
  font-src 'self' https://fonts.gstatic.com;
  img-src 'self' data: https: blob:;
  connect-src 'self' https://api.stripe.com https://vitals.vercel-insights.com;
  frame-src 'self' https://js.stripe.com;
  frame-ancestors 'none';

// Clean up whitespace const cspValue = ContentSecurityPolicy.replace(/\s{2,}/g, ' ').trim()

const securityHeaders = [ // ... other headers { key: 'Content-Security-Policy', value: cspValue } ]

CSP Directives Explained

DirectiveControls
default-srcFallback for all others
script-srcJavaScript sources
style-srcCSS sources
img-srcImage sources
font-srcFont sources
connect-srcFetch/XHR/WebSocket
frame-srciframe sources
frame-ancestorsWho can frame you

Dealing with 'unsafe-inline' and 'unsafe-eval'

Next.js requires these by default. To remove them:

Option 1: Use nonces (complex)

javascript
// middleware.ts
import { NextResponse } from 'next/server'
import crypto from 'crypto'

export function middleware(request) { const nonce = crypto.randomBytes(16).toString('base64') const csp = script-src 'nonce-${nonce}' 'strict-dynamic';

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

return response }

Option 2: Accept 'unsafe-inline' with other protections

For most apps, other headers provide sufficient protection.

Testing Your Headers

Using curl

bash
curl -I https://your-site.com

Using Security Header Tools

  1. securityheaders.com - Free scanner
  2. observatory.mozilla.org - Mozilla's tool
  3. Chrome DevTools - Network tab → Response Headers

Expected Output

HTTP/2 200
strict-transport-security: max-age=63072000; includeSubDomains; preload
x-frame-options: SAMEORIGIN
x-content-type-options: nosniff
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=()
content-security-policy: default-src 'self'; ...

Environment-Specific Headers

javascript
// next.config.js

const isDev = process.env.NODE_ENV === 'development'

const securityHeaders = [ // Always include { key: 'X-Content-Type-Options', value: 'nosniff' }, // Only in production ...(!isDev ? [ { key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubDomains; preload' } ] : []) ]

Common Issues

Stripe Elements Not Loading

Add to CSP:

frame-src https://js.stripe.com;
script-src https://js.stripe.com;

Google Fonts Not Loading

Add to CSP:

style-src https://fonts.googleapis.com;
font-src https://fonts.gstatic.com;

Analytics Blocked

Add to CSP:

script-src https://www.googletagmanager.com;
connect-src https://www.google-analytics.com;

Images Not Loading

img-src 'self' data: https:;  // Allow all HTTPS images
// or specific domains
img-src 'self' https://images.unsplash.com;

Complete next.config.js

javascript
// next.config.js

const ContentSecurityPolicy = default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline' https://www.googletagmanager.com https://js.stripe.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https: blob:; connect-src 'self' https://api.stripe.com https://vitals.vercel-insights.com https://www.google-analytics.com; frame-src 'self' https://js.stripe.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self';

const securityHeaders = [ { key: 'Content-Security-Policy', value: ContentSecurityPolicy.replace(/\s{2,}/g, ' ').trim() }, { key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubDomains; preload' }, { 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=(), interest-cohort=()' } ]

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

Checklist

[ ] HSTS configured (production only)
[ ] X-Frame-Options set
[ ] X-Content-Type-Options set
[ ] Referrer-Policy set
[ ] Permissions-Policy set
[ ] CSP configured and tested
[ ] All external services allowed in CSP
[ ] Headers verified with curl or online tool

The Bottom Line

Security headers are copy-paste security. Add them to next.config.js, test they work, ship it.

5 minutes of configuration. Significant protection against common attacks.

Ready to secure your AI-generated code?

Stop reading about vulnerabilities. Start fixing them.

Start Scanning Free