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.
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
| Header | Main job | Important caution |
|---|---|---|
Content-Security-Policy | Restricts content sources, framing, forms, and more | Must match the application and providers |
X-Content-Type-Options | Disables MIME sniffing with nosniff | Serve correct Content-Type values |
Referrer-Policy | Controls referrer information | Choose privacy versus attribution deliberately |
Permissions-Policy | Limits browser features | Audit embedded frames and real feature use |
Strict-Transport-Security | Forces future HTTPS access | HTTPS-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.
Where Should Headers Be Configured?
next.config.ts
Good for stable response headers and a static policy. Next.js evaluates matching header rules before the filesystem.
proxy.ts
Use when a fresh request nonce must exist before dynamic rendering and reach both the request and response.
Edge or reverse proxy
Useful for transport-wide policy, but establish one owner and test the final response to avoid accidental duplication.
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.
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.
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 nextConfigContent 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.

Educational Baseline CSP
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.
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.
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.frame-src → approved video or payment frameframe-ancestors → your pageCSP 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.
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.
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.
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.
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
| Integration | Inspect | Likely directives |
|---|---|---|
| Analytics / Tag Manager | Loader, requests, preview/debug mode, consent manager | script-src, connect-src, sometimes img-src and frame-src |
| Authentication / OAuth | Hosted UI, popup, redirects, API, avatar domains | script-src, connect-src, frame-src, form-action, img-src |
| Payments | Provider script, hosted fields, checkout redirect, telemetry | script-src, frame-src, connect-src, form-action |
| Video / maps / support | Embeds, thumbnails, workers, media, APIs | frame-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.
CSP for Images, Fonts, APIs, and WebSockets
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.
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 calls
Same-origin Route Handlers fit 'self'. External REST, GraphQL, telemetry, and streaming endpoints need exact connect-src origins.
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.
Example resource inventory
| Resource type | Detected source | Meaning for this article |
|---|---|---|
| Scripts | 'self', www.googletagmanager.com | Local JavaScript plus Google tag loaders |
| Analytics | www.googletagmanager.com; Google Analytics measurement ID configured | Network collection destinations must be observed before enforcing CSP |
| Monitoring | No browser monitoring provider detected | Do not invent a monitoring origin |
| Authentication | No provider detected | No 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 APIs | No browser API origin detected in the audited publishing path | Keep examples fictional and explicit |
| WebSockets | None detected | Do not add broad wss: |
| Frames / embeds | www.googletagmanager.com noscript frame in legacy blog index | Test or remove the duplicate legacy integration before policy enforcement |
| Payments | None detected | No 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.

Map scripts, styles, images, fonts, frames, forms, APIs, WebSockets, workers, and redirects.
Deploy a candidate Report-Only policy and sample production-like journeys.
Separate required resources, browser extensions, attack noise, stale code, and false assumptions.
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.
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.
Strict-Transport-Security: max-age=31536000X-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
| Question | CSP | CORS |
|---|---|---|
| Browser resource restrictions? | Yes, through content directives | Different purpose |
| Cross-origin API response sharing? | Not equivalent | Yes, when the responding server permits the requesting origin |
| Authentication? | No | No |
| Server-to-server protection? | No | No |
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.
Security Headers Behind Traefik, Nginx, or Vercel
| Layer | Good ownership candidate | Verify |
|---|---|---|
| Next.js | Application-aware CSP, route-specific policies, nonce generation | Dynamic rendering, framework scripts, route matchers |
| Traefik / Nginx | Transport-wide baseline and HSTS after TLS termination | Replacement versus append behavior and error responses |
| Vercel | next.config or platform controls | Preview 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.
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-srcwithframe-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
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.
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.
Related Next.js Tutorials
- Blog #27: Next.js 16 CSRF Protection
- Blog #11: Metadata & SEO
- Blog #12: Image Optimization
- Blog #13: Authentication
- Blog #14: Performance
- Blog #16: Environment Variables & Security
- Blog #17: Proxy
- Blog #18: Forms & Validation
- Blog #20: Docker + Traefik
- Blog #24: Monitoring & Observability
- Blog #25: Production Scaling


