Next.js security headers checklist: what to ship first (HSTS, CSP Report-Only, COOP/COEP)
securitywebops

Next.js security headers checklist: what to ship first (HSTS, CSP Report-Only, COOP/COEP)

4 min read

A practical checklist for security headers in Next.js. Start with low-breakage headers, add HSTS only when HTTPS is guaranteed, stage CSP in Report-Only, and apply COOP/COEP/CORP only on routes that need cross-origin isolation.

Table of Contents

What security headers should you ship in Next.js, and in what order?

Conclusion

Ship security headers in this order (low-breakage → high-breakage):

  1. Safe baseline headers (nosniff, referrer policy, permissions policy)
  2. HSTS (only when HTTPS is guaranteed everywhere)
  3. CSP in Report-Only (observe → tighten → enforce)
  4. COOP/COEP/CORP only on routes that truly need cross-origin isolation

The practical rule: one header change per deploy, with rollback.

Explanation

Security headers are a contract between your app and the browser. The risk is not “adding headers”. The risk is breaking production flows:

  • auth redirects
  • analytics tags
  • embedded content
  • CDNs/fonts/images

Treat header changes as ops rollouts:

  • stage
  • measure
  • tighten
  • enforce

Practical Guide

Step 1: inventory external dependencies (before CSP)

List what your app loads cross-origin:

  • analytics (GA4/GTM)
  • images/CDN (Cloudinary/S3/img proxy)
  • fonts (Google Fonts/self-host)
  • embeds (YouTube/Stripe/maps)
  • auth redirects

This inventory becomes your allowlist.

Step 2: ship safe baseline headers (usually low risk)

Common safe set:

  • X-Content-Type-Options: nosniff
  • Referrer-Policy: strict-origin-when-cross-origin
  • Permissions-Policy: ...
  • X-Frame-Options: DENY (only if you never embed)

Next.js example:

// next.config.ts
import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
  async headers() {
    return [
      {
        source: '/(.*)',
        headers: [
          { key: 'X-Content-Type-Options', value: 'nosniff' },
          { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
          { key: 'X-Frame-Options', value: 'DENY' },
          {
            key: 'Permissions-Policy',
            value: 'camera=(), microphone=(), geolocation=(), payment=()',
          },
        ],
      },
    ];
  },
};

export default nextConfig;

Decision rule:

  • If you need embedding, avoid X-Frame-Options: DENY and use CSP frame-ancestors instead.

Step 3: add HSTS (only if HTTPS is guaranteed)

HSTS can lock users into HTTPS, which is good—until you still have HTTP somewhere.

Example:

  • Strict-Transport-Security: max-age=15552000; preload

Decision rule:

  • Do not ship HSTS if any subdomain or endpoint is still HTTP.

Step 4: add CSP in Report-Only mode

Start with:

  • Content-Security-Policy-Report-Only

Do not start with enforce mode.

Minimal starter:

default-src 'self';
base-uri 'self';
object-src 'none';
frame-ancestors 'none';
img-src 'self' data: https:;
script-src 'self' 'unsafe-inline' https:;
style-src 'self' 'unsafe-inline' https:;

Then tighten based on reports:

  • reduce 'unsafe-inline'
  • pin hosts (avoid permanent https:)
  • migrate scripts toward nonces/hashes

Step 5: apply COOP/COEP/CORP only where needed

These are for cross-origin isolation (e.g., SharedArrayBuffer). They are easy to break.

  • Cross-Origin-Opener-Policy: same-origin
  • Cross-Origin-Embedder-Policy: require-corp
  • Cross-Origin-Resource-Policy: same-site

Decision rule:

  • Apply them only on routes that require cross-origin isolation (e.g. /app/isolation/*).

Step 6: rollout strategy (ops)

  • ship one change per deploy
  • keep a rollback toggle
  • monitor errors and business flows

Pitfalls

  • Copy-pasting a strict CSP and breaking auth/analytics
  • Enabling HSTS while HTTP still exists somewhere
  • Enabling COEP globally and breaking embeds/CDNs
  • Using X-Frame-Options while you need embedding

Checklist

  • [ ] External dependencies are inventoried (analytics/auth/cdn/fonts/embeds)
  • [ ] Safe baseline headers are shipped
  • [ ] X-Frame-Options decision is made (embed needed or not)
  • [ ] HTTPS guarantee is verified before shipping HSTS
  • [ ] HSTS is staged (max-age strategy) and documented
  • [ ] CSP starts in Report-Only
  • [ ] CSP reports are collected and reviewed
  • [ ] CSP allowlists are tightened (hosts pinned)
  • [ ] Script strategy exists (externalize / nonce / hashes)
  • [ ] COOP/COEP/CORP are scoped only to isolation routes (if used)
  • [ ] Rollback toggle exists for each header group

FAQ

Q1. Can I just use a package like helmet and be done?

It helps, but you still need decisions: embedding policy, CSP allowlists, and rollout strategy. The hard part is app-specific.

Q2. Why should CSP start in Report-Only?

Because CSP is app-specific. Report-Only lets you observe real dependencies before enforcing and breaking flows.

Q3. When do I need COOP/COEP?

Only when you need cross-origin isolation features (like SharedArrayBuffer). If you don’t know, don’t ship them globally.

References

Popular

  1. 1Permit2 explained (Web3): why approvals changed and how to use it safely (checklist)
  2. 2Read wallet signing screens (Web3): a 30-second checklist to avoid permission traps
  3. 3Spec-to-implementation prompt template (AI development): how to stop the model from guessing
  4. 4Revoke token approvals on EVM: how to audit allowances safely (checklist)
  5. 5Clarifying questions checklist (AI development): what to ask before you let an LLM build

Related Articles