Skip to main content
Security & Reliability · Blog 26

Next.js 16 Security Headers and Content Security Policy

Harden browser-facing responses without silently breaking framework scripts, analytics, images, fonts, authentication, or production traffic.

Server sending security response headers through a luminous shield to a browser application

In Blog #25, we scaled our Next.js application across multiple containers. More infrastructure means a larger security surface. The next step is hardening browser-facing responses with security headers and a carefully designed Content Security Policy.

Architecture audit before advice

This publishing repository is a PHP/Apache build, not the example Next.js application. It has no root package.json, next.config.*, proxy.ts, Docker, Traefik, Nginx, or Vercel configuration, so an installed Next.js patch version and runtime policy cannot be verified here. The site currently sets four baseline headers in .htaccess, loads Google Analytics through gtag.js, contains a separate legacy Tag Manager loader, pauses AdSense, serves local fonts and images, and exposes no discovered auth or payment provider. The examples below are reviewable templates, not a claim that any application is fully secure.

What Are Security Headers in Next.js?

Security headers are HTTP response headers that tell browsers how to handle risky content and capabilities. In Next.js, correctly configured CSP, X-Content-Type-Options, Referrer-Policy, and Permissions-Policy can reduce exposure to script injection, clickjacking, MIME confusion, and unnecessary browser features as part of defense in depth.

Security Headers at a Glance

Diagram 1: Browser-facing security policyThe application or its trusted edge adds one reviewed set of response headers, and the browser enforces those rules.
HeaderMain jobImportant caution
Content-Security-PolicyRestricts content sources, framing, forms, and moreMust match the application and providers
X-Content-Type-OptionsDisables MIME sniffing with nosniffServe correct Content-Type values
Referrer-PolicyControls referrer informationChoose privacy versus attribution deliberately
Permissions-PolicyLimits browser featuresAudit embedded frames and real feature use
Strict-Transport-SecurityForces future HTTPS accessHTTPS-only; subdomains and preload expand impact

Why Security Headers Matter

The browser receives markup, scripts, styles, images, frames, and network instructions from your response. Headers add enforceable browser rules around those resources. They can limit the impact of some injection bugs and unsafe embedding, but they do not repair vulnerable code or replace validation, output encoding, authentication, authorization, secure cookies, dependency updates, and monitoring.

Diagram 2: Defense in depthSecurity headers surround, but do not replace, secure application behavior and infrastructure controls.
Validated inputAuthorizationSafe HTML + browser headersTLS + monitoring

Where Should Headers Be Configured?

01

next.config.ts

Good for stable response headers and a static policy. Next.js evaluates matching header rules before the filesystem.

02

proxy.ts

Use when a fresh request nonce must exist before dynamic rendering and reach both the request and response.

03

Edge or reverse proxy

Useful for transport-wide policy, but establish one owner and test the final response to avoid accidental duplication.

04

Route Handler

Useful for route-specific API responses, CORS, downloads, and reporting endpoints rather than a fragmented global policy.

If Next.js and a proxy set the same ordinary header, the downstream layer may replace or append it. Multiple CSP headers are different: browsers enforce every policy, so the effective result becomes more restrictive. Keep an ownership table for application, proxy, CDN, and platform headers.

Diagram 3: Header configuration layersA response may cross application, reverse-proxy, CDN, and browser layers; one documented owner should set each policy.
Next.jsTraefik / NginxCDN / platformBrowser receives final headers

Security Headers in next.config

Start with low-breakage headers, test them, and add CSP separately. This sample intentionally leaves out HSTS until HTTPS coverage and subdomains are verified.

next.config.ts — reviewed baseline
import type { NextConfig } from 'next'

const securityHeaders = [
  { key: 'X-Content-Type-Options', value: 'nosniff' },
  { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
  {
    key: 'Permissions-Policy',
    value: 'camera=(), microphone=(), geolocation=()',
  },
  // Compatibility fallback; CSP frame-ancestors is the modern control.
  { key: 'X-Frame-Options', value: 'DENY' },
]

const nextConfig: NextConfig = {
  poweredByHeader: false,
  headers() {
    return [{ source: '/(.*)', headers: securityHeaders }]
  },
}

export default nextConfig

Content Security Policy

CSP is a response policy composed of directives. Each directive controls a different browser capability. A production policy should be derived from your routes, rendering mode, inline code, analytics, authentication, payments, media, fonts, images, API calls, workers, and frames. Avoid widening default-src just to silence one violation.

Trusted scripts, styles, images, fonts, connections and frames entering a browser while untrusted resources are blocked outside the CSP boundary
A CSP is a trust boundary. Permit the smallest reviewed source set per resource type and keep unknown origins outside it.
Diagram 4: CSP enforcement decisionFor each browser resource request, the matching directive either permits the exact source or blocks it and records a violation.
Resource requestFind matching directiveCompare source
Allowed: loadDenied: block + report

Educational Baseline CSP

Example only

Do not paste this into production without adapting it to the application’s actual Next.js runtime, analytics, authentication, images, fonts, APIs, frames, forms, WebSockets, payments, and other third-party services. Begin with reporting and critical-flow tests.

Provider-free baseline for analysis
default-src 'self';
script-src 'self';
style-src 'self';
img-src 'self' data: blob:;
font-src 'self';
connect-src 'self';
frame-src 'none';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';

This baseline is intentionally restrictive and may block Next.js inline bootstrapping or styles, depending on rendering and build behavior. It may also block every external integration. The correct response to those violations is investigation: choose a supported nonce or hash architecture, remove unnecessary inline code, or add only the exact justified sources. Do not jump directly to script-src * or broad 'unsafe-inline'.

CSP Directives Explained

script-src

Controls JavaScript sources and inline script execution. Prefer nonces or hashes over 'unsafe-inline'; keep 'unsafe-eval' out of production unless a verified dependency makes the risk unavoidable.

style-src

Controls stylesheets and inline styles. Inventory CSS-in-JS and framework output before removing an exception. A script nonce does not automatically solve every inline-style case.

img-src

Allow the exact image origins required by next/image, user content, analytics pixels, and object URLs. data: and blob: are separate source expressions.

font-src

Self-hosted next/font assets can often use 'self'. External font providers require their exact font-file origin, not only the stylesheet origin.

connect-src

Controls fetch, XHR, EventSource, WebSocket, and similar script connections. Add explicit https: or wss: origins only when needed.

frame-src

Controls which frames your page may load, such as a payment or video embed. It does not control who may frame your site.

frame-ancestors

Controls which parent pages may embed your response. It has no default-src fallback and cannot be delivered through a CSP meta element.

object-src

Use 'none' unless legacy plugin content is explicitly required and reviewed.

base-uri

Restricts values accepted by the document’s <base> element. 'self' or 'none' reduces base-URL injection impact.

form-action

Restricts form submission targets. Include verified external identity or payment targets only when a real browser form posts to them.

default-src

Fallback for fetch directives that are absent. It does not replace navigation directives such as frame-ancestors or form-action.

Diagram 5: script-src trust choicesA script is accepted only through an approved same-origin file, exact trusted source, or matching per-response nonce or content hash.
Script
Same originExact trusted hostMatching nonceMatching hash
Execute
Diagram 6: Frame controls point in opposite directionsframe-src controls frames loaded inside your page; frame-ancestors controls which parent pages may embed your response.
Your page loads outwardframe-src → approved video or payment frame
Parent embeds inwardapproved parent → frame-ancestors → your page

CSP Nonces in Next.js 16

A nonce is an unpredictable, one-use value generated for each response. Current Next.js guidance creates it in proxy.ts, places it in the CSP request and response headers, and dynamically renders the protected page. Next.js can then apply the matching nonce to framework and page scripts.

Diagram 7: Per-response nonce flowThe server creates a fresh nonce, places the same value in the CSP and approved script, and the browser executes only a match.
RequestGenerate unpredictable nonce
CSP headerApproved script
Match: executeMismatch: block
Rendering trade-off

Request-specific nonces require dynamic rendering. Static optimization and ISR are disabled for those pages, CDN caching needs special care, and current Next.js guidance says Partial Prerendering is incompatible with nonce-based CSP because the static shell cannot receive the request nonce.

proxy.ts — start in Report-Only mode
import { NextRequest, NextResponse } from 'next/server'

export function proxy(request: NextRequest) {
  const nonce = Buffer.from(crypto.randomUUID()).toString('base64')
  const isDev = process.env.NODE_ENV === 'development'
  const policy = `
    default-src 'self';
    script-src 'self' 'nonce-${nonce}' 'strict-dynamic'${
      isDev ? " 'unsafe-eval'" : ''
    };
    style-src 'self' 'nonce-${nonce}';
    img-src 'self' blob: data:;
    font-src 'self';
    connect-src 'self';
    frame-src 'none';
    frame-ancestors 'none';
    object-src 'none';
    base-uri 'self';
    form-action 'self';
  `.replace(/\s{2,}/g, ' ').trim()

  const requestHeaders = new Headers(request.headers)
  requestHeaders.set('x-nonce', nonce)
  const response = NextResponse.next({ request: { headers: requestHeaders } })
  response.headers.set('Content-Security-Policy-Report-Only', policy)
  return response
}

export const config = {
  matcher: [{
    source: '/((?!api|_next/static|_next/image|favicon.ico).*)',
    missing: [
      { type: 'header', key: 'next-router-prefetch' },
      { type: 'header', key: 'purpose', value: 'prefetch' },
    ],
  }],
}

Do not copy that provider-free policy directly into enforcement. It deliberately blocks external analytics, authentication, payments, media, remote images, WebSockets, and APIs until their exact requirements are reviewed.

Hash-Based CSP

A hash allows one exact inline script or style. It suits content that is stable across responses; any byte change, including whitespace, changes the hash. Build tooling can calculate hashes, but manually maintained hashes become fragile. Next.js also documents experimental Subresource Integrity support for hash-oriented deployments, so verify the current limitations before choosing it.

Diagram 8: Hash-based script approvalBuild tooling hashes known script bytes; the browser hashes the delivered script and executes it only when the policy value matches.
Known script bytesSHA-256 digest in CSPBrowser-computed digestExact match

Strict CSP Concepts

A strict policy establishes trust with a nonce or hash rather than a broad host list. 'strict-dynamic' can extend that trust to scripts loaded by an already trusted script in supporting browsers. Keep a compatible source strategy for the browsers you support, and test actual script-loading behavior rather than judging policy strength by length.

CSP and Next.js Script

Read the nonce in a Server Component and pass it to a third-party Script. The provider’s host may still need to be represented for browser compatibility and its network calls belong in connect-src.

Server Component
import { headers } from 'next/headers'
import Script from 'next/script'

export default async function Analytics() {
  const nonce = (await headers()).get('x-nonce') ?? undefined
  return (
    <Script
      src="https://www.googletagmanager.com/gtag/js?id=G-EXAMPLE"
      strategy="afterInteractive"
      nonce={nonce}
    />
  )
}

CSP with Analytics, Authentication, and Third Parties

IntegrationInspectLikely directives
Analytics / Tag ManagerLoader, requests, preview/debug mode, consent managerscript-src, connect-src, sometimes img-src and frame-src
Authentication / OAuthHosted UI, popup, redirects, API, avatar domainsscript-src, connect-src, frame-src, form-action, img-src
PaymentsProvider script, hosted fields, checkout redirect, telemetryscript-src, frame-src, connect-src, form-action
Video / maps / supportEmbeds, thumbnails, workers, media, APIsframe-src, img-src, media-src, worker-src, connect-src

Do not add a vendor’s entire wildcard domain from memory. Open every important user journey in a production-like environment, record the origins, compare them with current provider documentation, and add the smallest stable set. Re-test login, logout, refresh, account linking, checkout, consent changes, and error flows.

Diagram 9: Third-party allowlist reviewA provider is decomposed into the exact browser resources it uses instead of receiving a broad wildcard exception.
Real integration
loader hostAPI hostframe hostimage host
Small directive-specific allowlist

CSP for Images, Fonts, APIs, and WebSockets

IMG

Images

CSP is separate from Next.js images.remotePatterns. Both must allow the source. Include data: only for real data URLs and blob: only for object-URL workflows.

FONT

Fonts

next/font self-hosting simplifies policy. If CSS comes from one external origin and font files from another, allow them in style-src and font-src respectively.

API

API calls

Same-origin Route Handlers fit 'self'. External REST, GraphQL, telemetry, and streaming endpoints need exact connect-src origins.

WS

WebSockets

Add the explicit wss:// production origin. Test the secure scheme, port, reconnect path, and every environment separately.

Browser Web Workers use CSP controls such as worker-src. They are unrelated to the server-side queue workers introduced in Blog #23. Do not add a browser worker origin because a BullMQ or Redis worker runs on the server.

Diagram 10: Resource inventory becomes policyObserved browser resources are grouped by type, mapped to exact origins, and converted into narrow CSP directives.
scriptsstylesimagesfonts
connectionsframesformsworkers
Reviewed CSP

Detected resource inventory for this publishing repository

Resource typeDetected sourceMeaning for this article
Scripts'self', www.googletagmanager.comLocal JavaScript plus Google tag loaders
Analyticswww.googletagmanager.com; Google Analytics measurement ID configuredNetwork collection destinations must be observed before enforcing CSP
MonitoringNo browser monitoring provider detectedDo not invent a monitoring origin
AuthenticationNo provider detectedNo auth domain can be safely pre-allowed
Images'self'Published assets are local; remote image providers were not detected
Fonts'self'Font files are stored under local assets
External APIsNo browser API origin detected in the audited publishing pathKeep examples fictional and explicit
WebSocketsNone detectedDo not add broad wss:
Frames / embedswww.googletagmanager.com noscript frame in legacy blog indexTest or remove the duplicate legacy integration before policy enforcement
PaymentsNone detectedNo payment origin can be safely pre-allowed

Report-Only Mode and Violation Reporting

Content-Security-Policy-Report-Only reports violations without blocking them. Use it to learn what the application actually requires, but remember that report-only success does not prove enforcement is safe. Exercise real routes, roles, locales, error states, and browser families.

Three-stage CSP rollout moving from browser reporting through violation review to enforced trusted traffic
Diagram 11: Report, review, then enforce. Classify every violation, fix code or narrow the allowlist, deploy to a small audience, and keep monitoring after enforcement.
1. Inventory

Map scripts, styles, images, fonts, frames, forms, APIs, WebSockets, workers, and redirects.

2. Report

Deploy a candidate Report-Only policy and sample production-like journeys.

3. Classify

Separate required resources, browser extensions, attack noise, stale code, and false assumptions.

4. Enforce

Canary the reviewed policy, watch errors and conversion paths, then expand gradually.

A report endpoint is an internet-facing ingestion service. Authenticate where possible, cap body size and rate, validate JSON, avoid logging sensitive document URLs or samples unnecessarily, and never render report values as trusted HTML. Support for report-to, Reporting-Endpoints, and legacy report-uri varies, so test your browser matrix.

X-Content-Type-Options, Referrer-Policy, and Permissions-Policy

X-Content-Type-Options: nosniff

This tells browsers to respect declared MIME types instead of guessing. It works with correct server Content-Type configuration; test scripts, styles, uploads, and downloads.

Referrer-Policy: strict-origin-when-cross-origin

This retains the full referrer for same-origin requests, sends only the origin on secure cross-origin requests, and omits it on HTTPS-to-HTTP downgrades. Choose no-referrer or a different policy only after reviewing privacy, analytics, and partner requirements.

Permissions-Policy

Disable browser capabilities the application does not use. Start from a feature inventory rather than copying a huge list; syntax and browser support differ by directive, and embedded frames may need their own allow attributes.

Diagram 12: Referrer-Policy trade-offA same-origin navigation may retain the full path, while a cross-origin navigation can send only the origin or no referrer, depending on policy.
Current page
same origin → full referrercross origin → origin onlydowngrade → no referrer
Diagram 13: Permissions-Policy gateThe document and its embedded frames receive only the browser capabilities intentionally allowed for their origins.
cameramicrophonegeolocation
Origin allowlist
Required: allowUnused: deny

HSTS

Strict-Transport-Security tells a browser to use HTTPS for future requests. Send it only over HTTPS. Begin with a short reviewed max-age, monitor, and increase it gradually. Add includeSubDomains only when every required subdomain supports HTTPS. Treat preload as a separate, long-lived operational commitment.

Example after HTTPS review
Strict-Transport-Security: max-age=31536000
Diagram 14: HSTS changes future navigationAfter a valid HTTPS response teaches the browser an HSTS policy, future HTTP attempts for that host are upgraded locally until max-age expires.
HTTPS response + HSTSBrowser remembers hostFuture HTTP attemptUpgrade to HTTPS

X-Frame-Options vs frame-ancestors

frame-ancestors is the modern, flexible framing control and can allow several explicit parents. X-Frame-Options remains a simple compatibility fallback using DENY or SAMEORIGIN. Keep both semantically aligned. Do not confuse frame-ancestors, which controls who embeds you, with frame-src, which controls frames you load.

Cross-Origin Headers

Cross-Origin-Opener-Policy, Cross-Origin-Embedder-Policy, and Cross-Origin-Resource-Policy can enable strong isolation, but they can also break OAuth popups, payment windows, embedded tools, images, fonts, and resources lacking compatible CORS or CORP responses. Add them only for a documented isolation requirement and test every cross-origin flow.

CSP vs CORS

QuestionCSPCORS
Browser resource restrictions?Yes, through content directivesDifferent purpose
Cross-origin API response sharing?Not equivalentYes, when the responding server permits the requesting origin
Authentication?NoNo
Server-to-server protection?NoNo

CSP tells a browser what a protected document may load or do. CORS tells a browser whether JavaScript from one origin may read a response from another origin. Neither proves identity, authorizes a user, validates a webhook, or prevents a server from making an outbound request.

Diagram 15: CSP and CORS answer different questionsCSP starts from a protected page and limits its resources; CORS starts from a responding API and controls whether another origin may read that response.
CSPPage → may load this source?
CORSAPI → may this origin read me?

Security Headers Behind Traefik, Nginx, or Vercel

LayerGood ownership candidateVerify
Next.jsApplication-aware CSP, route-specific policies, nonce generationDynamic rendering, framework scripts, route matchers
Traefik / NginxTransport-wide baseline and HSTS after TLS terminationReplacement versus append behavior and error responses
Vercelnext.config or platform controlsPreview versus production, redirects, edge responses, platform defaults

Inspect the final public response, not only source configuration. A CDN can cache an old header, a proxy can add another one, and framework routes, static files, API errors, and redirects may follow different paths.

Diagram 16: Static policy vs dynamic nonce policyA static policy is identical across responses and cache-friendly; a nonce policy changes per request and requires dynamic rendering.
Static CSPone reviewed valueevery responsestatic/CDN friendly
Nonce CSPfresh value per requestdynamic renderhigher compute + cache trade-off

Common CSP Mistakes

  • Enforcing a copied policy before inventory and Report-Only observation.
  • Adding 'unsafe-inline' or * to make console errors disappear.
  • Using one broad directive instead of narrow resource-specific sources.
  • Forgetting analytics collection, OAuth, payments, fonts, images, workers, or WebSockets.
  • Reusing a nonce across responses or generating predictable values.
  • Adding a nonce to every script through string replacement, including injected scripts.
  • Expecting nonce-based pages to remain statically rendered and CDN cacheable.
  • Confusing frame-src with frame-ancestors.
  • Setting independent CSP headers at several infrastructure layers.
  • Enabling HSTS preload before all subdomains are permanently HTTPS-ready.
  • Collecting violation reports without rate limits, redaction, retention, or ownership.
  • Treating a security scanner grade as proof that application logic is secure.

Testing Security Headers

Inspect production-like responses
curl -I https://example.com/
curl -I https://example.com/login
curl -I https://example.com/api/health

# Check for accidental duplicates
curl -sS -D - -o /dev/null https://example.com/ \
  | grep -iE 'content-security|strict-transport|x-content|referrer|permissions|x-frame'

Then use browser DevTools: clear cache, preserve the network log, visit every critical route, inspect response headers, filter CSP console messages, and complete analytics, login, logout, upload, checkout, embed, error, and WebSocket flows. Add integration assertions for representative document and API responses.

Diagram 17: Production CSP testing loopA candidate policy moves through Report-Only observation, critical-flow tests, violation review, fixes, enforcement, monitoring, and the next revision.
PolicyReport-OnlyTestReviewFixEnforceMonitor & repeat

Production Checklist

  • Record the exact Next.js and provider versions in the real application repository.
  • Map which layer owns every response header.
  • Inventory all resource and connection origins by environment.
  • Choose static, nonce-based, or hash-based CSP from rendering requirements.
  • Generate a fresh unpredictable nonce for every dynamic response.
  • Keep development-only 'unsafe-eval' out of production.
  • Deploy CSP in Report-Only mode and exercise critical journeys.
  • Classify and redact violation data; rate-limit the report endpoint.
  • Canary enforcement and monitor frontend errors and business flows.
  • Verify correct MIME types with nosniff.
  • Review referrer privacy and Permissions-Policy features.
  • Enable HSTS gradually only after complete HTTPS validation.
  • Test framing, OAuth popups, payments, embeds, APIs, and WebSockets.
  • Re-audit the policy whenever a dependency or third-party provider changes.

Frequently Asked Questions

What security headers should a Next.js application use?

A reviewed baseline commonly includes Content-Security-Policy, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, and HSTS on HTTPS-only deployments. Frame protection can use CSP frame-ancestors, with X-Frame-Options retained as a compatibility fallback when appropriate.

What is CSP in Next.js?

Content Security Policy is a browser-enforced response policy that restricts where scripts, styles, images, fonts, frames, connections, and other resources may come from. It is a defense-in-depth control, not a replacement for secure application code.

How do I add security headers in Next.js?

Use headers() in next.config for stable route-matched headers. Use request-time handling such as proxy.ts when a fresh CSP nonce must be generated before dynamic rendering. Always inspect the final public response after proxy and platform processing.

Should headers be configured in Next.js or Traefik?

Choose one documented owner per header. Stable transport-wide headers can live at the trusted edge, while request-specific nonce CSP generally belongs in the application request path. Avoid accidental duplicate CSP policies.

What does script-src do?

script-src controls the JavaScript sources and inline script execution methods that a browser accepts. Keep the source set minimal and prefer a reviewed nonce- or hash-based design over wildcards or unsafe-inline.

Should I use unsafe-inline?

Do not add unsafe-inline as an automatic production fix. First inventory the inline code and framework behavior, then use nonces, hashes, or a carefully justified compatibility plan. Test the candidate policy in Report-Only mode.

What is a CSP nonce?

A nonce is an unpredictable one-time value included in a response policy and on approved scripts or styles. Current Next.js guidance generates a fresh nonce per request and uses dynamic rendering so the framework can apply it.

Can I reuse the same CSP nonce?

No. A production nonce should be unpredictable and unique for each response. A static or reused value loses the security property that makes a nonce useful.

Does CSP prevent all XSS?

No. CSP can reduce the impact of some injection paths, but it is an additional layer. Output encoding, safe DOM APIs, validation, authorization, dependency hygiene, and other XSS defenses are still required.

What is frame-ancestors?

frame-ancestors controls which parent pages may embed the response. Use none when embedding is not required or an explicit source list when it is. It is different from frame-src, which controls frames loaded by your page.

What does X-Content-Type-Options do?

With the value nosniff, it tells browsers to respect declared Content-Type values rather than MIME-sniffing content. The server must still send correct MIME types.

Which Referrer-Policy should I use?

Choose from the application privacy and attribution requirements. strict-origin-when-cross-origin is a common balanced choice, while no-referrer sends less information. Verify analytics and partner flows before changing it.

Should I enable HSTS?

Enable HSTS only after HTTPS is reliable. Start with a reviewed duration and add includeSubDomains or preload only after every affected subdomain and the recovery implications have been verified.

What is CSP Report-Only?

Content-Security-Policy-Report-Only observes and reports policy violations without blocking resources. It supports rollout analysis, but critical flows must still be tested again under actual enforcement.

Can CSP break authentication, analytics, or payments?

Yes. Those integrations may require exact script, connection, frame, image, and form destinations. Use current provider documentation plus network traces, and avoid giant speculative allowlists.

Current Official References

Next Steps

Begin with an origin inventory and header ownership map for the real Next.js deployment. Ship the low-breakage baseline, observe a route-specific CSP in Report-Only mode, remove unnecessary dependencies and exceptions, then canary enforcement. Re-run the review whenever analytics, authentication, images, fonts, payments, or deployment layers change.

WhatsApp