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.
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
| 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.
Detected resource inventory for this publishing repository
| 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 #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
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.js Content Security Policy guide
- Next.js
headers()reference - MDN Content Security Policy guide
- MDN Strict-Transport-Security reference
- MDN X-Content-Type-Options reference
- MDN Referrer-Policy reference
- MDN Permissions Policy guide
- OWASP Content Security Policy Cheat Sheet
- OWASP HTTP Security Response Headers Cheat Sheet
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.
