Security Headers

Patterns for HTTP security headers in Next.js applications.

Overview#

Security headers protect your application from common attacks like XSS, clickjacking, and MIME sniffing. This pattern covers:

  • Next.js headers configuration
  • Content Security Policy (CSP)
  • CORS configuration
  • Cache control for sensitive data

Prerequisites#

# No additional packages required

Code Example#

Next.js Headers Configuration#

1// next.config.js 2/** @type {import('next').NextConfig} */ 3const nextConfig = { 4 async headers() { 5 return [ 6 { 7 source: '/:path*', 8 headers: [ 9 { 10 key: 'X-DNS-Prefetch-Control', 11 value: 'on' 12 }, 13 { 14 key: 'Strict-Transport-Security', 15 value: 'max-age=63072000; includeSubDomains; preload' 16 }, 17 { 18 key: 'X-Frame-Options', 19 value: 'SAMEORIGIN' 20 }, 21 { 22 key: 'X-Content-Type-Options', 23 value: 'nosniff' 24 }, 25 { 26 key: 'Referrer-Policy', 27 value: 'strict-origin-when-cross-origin' 28 }, 29 { 30 key: 'Permissions-Policy', 31 value: 'camera=(), microphone=(), geolocation=()' 32 } 33 ] 34 } 35 ] 36 } 37} 38 39module.exports = nextConfig

Content Security Policy Builder#

1// lib/csp.ts 2type CspDirective = 3 | 'default-src' 4 | 'script-src' 5 | 'style-src' 6 | 'img-src' 7 | 'font-src' 8 | 'connect-src' 9 | 'media-src' 10 | 'object-src' 11 | 'frame-src' 12 | 'frame-ancestors' 13 | 'base-uri' 14 | 'form-action' 15 | 'upgrade-insecure-requests' 16 17export function buildCsp(directives: Partial<Record<CspDirective, string[]>>): string { 18 const defaults: Partial<Record<CspDirective, string[]>> = { 19 'default-src': ["'self'"], 20 'script-src': ["'self'"], 21 'style-src': ["'self'", "'unsafe-inline'"], 22 'img-src': ["'self'", 'data:', 'https:'], 23 'font-src': ["'self'"], 24 'connect-src': ["'self'"], 25 'frame-ancestors': ["'none'"], 26 'base-uri': ["'self'"], 27 'form-action': ["'self'"] 28 } 29 30 const merged = { ...defaults, ...directives } 31 32 return Object.entries(merged) 33 .map(([key, values]) => `${key} ${values!.join(' ')}`) 34 .join('; ') 35} 36 37// Usage with Stripe 38const csp = buildCsp({ 39 'script-src': ["'self'", 'https://js.stripe.com'], 40 'frame-src': ["'self'", 'https://js.stripe.com'], 41 'connect-src': ["'self'", 'https://api.stripe.com'] 42})

Middleware with Security Headers#

1// middleware.ts 2import { NextResponse } from 'next/server' 3import type { NextRequest } from 'next/server' 4import { buildCsp } from '@/lib/csp' 5 6export function middleware(request: NextRequest) { 7 const response = NextResponse.next() 8 9 // Generate nonce for inline scripts 10 const nonce = Buffer.from(crypto.randomUUID()).toString('base64') 11 12 const csp = buildCsp({ 13 'script-src': ["'self'", `'nonce-${nonce}'`], 14 'style-src': ["'self'", "'unsafe-inline'"], 15 'connect-src': [ 16 "'self'", 17 'https://api.stripe.com', 18 process.env.NEXT_PUBLIC_API_URL! 19 ] 20 }) 21 22 response.headers.set('Content-Security-Policy', csp) 23 response.headers.set('X-Nonce', nonce) 24 25 return response 26}

Using Nonce for Inline Scripts#

1// app/layout.tsx 2import { headers } from 'next/headers' 3 4export default async function RootLayout({ children }: { children: React.ReactNode }) { 5 const headersList = await headers() 6 const nonce = headersList.get('X-Nonce') ?? '' 7 8 return ( 9 <html lang="en"> 10 <head> 11 <script 12 nonce={nonce} 13 dangerouslySetInnerHTML={{ 14 __html: ` 15 window.__CONFIG__ = ${JSON.stringify({ 16 apiUrl: process.env.NEXT_PUBLIC_API_URL 17 })} 18 ` 19 }} 20 /> 21 </head> 22 <body>{children}</body> 23 </html> 24 ) 25}

CORS Headers#

1// lib/cors.ts 2const ALLOWED_ORIGINS = [ 3 'https://app.example.com', 4 'https://admin.example.com' 5] 6 7export function getCorsHeaders(origin: string | null) { 8 const headers: Record<string, string> = {} 9 10 if (origin && ALLOWED_ORIGINS.includes(origin)) { 11 headers['Access-Control-Allow-Origin'] = origin 12 headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS' 13 headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization' 14 headers['Access-Control-Allow-Credentials'] = 'true' 15 headers['Access-Control-Max-Age'] = '86400' 16 } 17 18 return headers 19} 20 21// Usage in API route 22export async function OPTIONS(request: Request) { 23 const origin = request.headers.get('origin') 24 const corsHeaders = getCorsHeaders(origin) 25 26 return new Response(null, { 27 status: 204, 28 headers: corsHeaders 29 }) 30}

Cache Control for Sensitive Data#

1// API responses with sensitive data 2export async function GET() { 3 const sensitiveData = await getSensitiveData() 4 5 return Response.json(sensitiveData, { 6 headers: { 7 'Cache-Control': 'no-store, no-cache, must-revalidate, private', 8 'Pragma': 'no-cache', 9 'Expires': '0' 10 } 11 }) 12} 13 14// Static/public data 15export async function GET() { 16 const publicData = await getPublicData() 17 18 return Response.json(publicData, { 19 headers: { 20 'Cache-Control': 'public, max-age=3600, s-maxage=3600', 21 'CDN-Cache-Control': 'public, max-age=86400' 22 } 23 }) 24}

Permissions Policy#

1// Restrict browser features 2const permissionsPolicy = [ 3 'camera=()', 4 'microphone=()', 5 'geolocation=()', 6 'interest-cohort=()', // Disable FLoC 7 'payment=(self)', 8 'usb=()', 9 'magnetometer=()', 10 'gyroscope=()', 11 'accelerometer=()' 12].join(', ') 13 14response.headers.set('Permissions-Policy', permissionsPolicy)

Usage Instructions#

  1. Add security headers in next.config.js for static configuration
  2. Use middleware for dynamic headers like CSP with nonces
  3. Configure CORS for API endpoints that need cross-origin access
  4. Set appropriate cache control for sensitive vs public data
  5. Use Permissions-Policy to disable unused browser features

Best Practices#

  • Start strict - Begin with restrictive policies and loosen as needed
  • Use nonces - Avoid unsafe-inline for scripts when possible
  • Test thoroughly - Security headers can break functionality
  • Monitor CSP violations - Use report-uri to track issues
  • HSTS preload - Submit your domain to the HSTS preload list
  • Review regularly - Update policies as your app evolves