Adding security headers to a Next.js site in 5 minutes
If you have ever run an audit on a brand-new Next.js site, you have seen the report: Missing Content Security Policy. Missing HSTS. Missing X-Frame-Options. The default Next.js setup ships with none of these. The good news is you fix all six in a single next.config.js change, with no extra packages or middleware. Here is the copy-paste version.
What we are adding and why
Modern browsers respect a set of response headers that tell the browser how to treat your page. Setting them correctly closes off entire categories of attacks. Not setting them leaves the door open for the lazy ones.
nosniff). Prevents browsers from guessing the MIME type of a response — closes a category of attacks where a file is delivered as one type but executed as another.The full next.config.js
Open next.config.js at the root of your Next.js project. If it does not exist, create it. Here is the complete file:
/** @type {import('next').NextConfig} */
const nextConfig = {
async headers() {
return [
{
source: '/:path*',
headers: [
{
key: 'Content-Security-Policy',
value: [
"default-src 'self'",
"script-src 'self' 'unsafe-inline'",
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
"img-src 'self' data: https:",
"font-src 'self' https://fonts.gstatic.com data:",
"connect-src 'self'",
"frame-src 'self'",
"object-src 'none'",
"base-uri 'self'",
"form-action 'self'",
"frame-ancestors 'self'",
"upgrade-insecure-requests",
].join('; '),
},
{ key: 'X-Frame-Options', value: 'SAMEORIGIN' },
{ key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
{
key: 'Permissions-Policy',
value: 'camera=(), microphone=(), geolocation=(), interest-cohort=()',
},
{
key: 'Strict-Transport-Security',
value: 'max-age=63072000; includeSubDomains; preload',
},
],
},
]
},
}
module.exports = nextConfigSave, commit, deploy. Six headers shipped at once. That is the whole job for a site that does not use external scripts, fonts, or API calls beyond your own backend.
Customizing CSP for your stack
The Content Security Policy is the one header where you almost certainly need to add domains. The CSP above is restrictive — it allows your own origin, Google Fonts, and inline styles (which Next.js requires for streaming HTML). If you use any third-party service, you need to add it.
Stripe
For Stripe Checkout, you need to allow their scripts and iframes:
"script-src 'self' 'unsafe-inline' https://js.stripe.com",
"frame-src 'self' https://js.stripe.com https://hooks.stripe.com",
"connect-src 'self' https://api.stripe.com",Supabase
If you use Supabase Auth or Storage, allow their domain on connect-src:
"connect-src 'self' https://*.supabase.co",Cloudflare Turnstile
Turnstile injects a script and renders inside an iframe:
"script-src 'self' 'unsafe-inline' https://challenges.cloudflare.com",
"frame-src 'self' https://challenges.cloudflare.com",Your own backend
If your frontend calls a separate backend (Railway, Fly, Render, whatever), add that domain to connect-src:
"connect-src 'self' https://api.yourdomain.com",Testing CSP without breaking the site
CSP errors are silent in production. The page renders, but a script that violated the policy was blocked, and your app silently fails. To catch this before users do, use Content-Security-Policy-Report-Only mode first.
Change the header key for a few days:
{ key: 'Content-Security-Policy-Report-Only', value: '...' }In report-only mode, the browser logs violations to the console but does not block anything. You ship to production, you open Browser DevTools, you check the console for Refused to load... messages. Each one is a domain you need to add to your policy.
Once the console is clean for a week of normal traffic, flip the header key back to Content-Security-Policy (without -Report-Only) to enforce it.
The HSTS gotcha
HSTS is powerful and irreversible. Once a browser sees the header, it will refuse to make HTTP requests to your domain for the duration of max-age — even if you later remove the header. If you set max-age=63072000 (2 years) and then break HTTPS for some reason, every browser that visited you will refuse to connect for two years.
Recommended approach:
- Start with a short max-age like
max-age=86400(1 day) to test - Verify HTTPS works perfectly across all subdomains and assets
- Bump to
max-age=31536000(1 year) - Bump to
max-age=63072000(2 years) and addpreloadif you want browser-baked-in HSTS
The includeSubDomains directive is also worth thinking about. If you have a subdomain that genuinely cannot be served over HTTPS (a legacy admin panel, say), do not set includeSubDomains until you have fixed that subdomain.
What to skip
Some headers exist but are not worth setting in 2026:
- X-XSS-Protection — deprecated. Modern browsers ignore it. CSP replaces it.
- Public-Key-Pins (HPKP) — deprecated and unsafe. Caused real outages when sites pinned the wrong cert. Use HSTS instead.
- Expect-CT — deprecated as of 2023. Certificate Transparency is now enforced by browsers automatically.
If a checker tool tells you that you are missing any of the above, ignore it — the checker is out of date.
Vercel-specific note
On Vercel, the headers() function in next.config.js applies to all routes, including static assets served from /public and all dynamic routes. No additional config needed.
One thing to verify: Vercel adds an X-Vercel-Cache header automatically, and your CSP does not need to allow it — your own headers are layered on top, not replaced.
Verifying it works
After deploying, three ways to check:
Option 1: Lintry
Run a scan on your URL. Lintry reports every missing or misconfigured security header with the recommended fix. Try it free.
Option 2: Browser DevTools
Open DevTools, go to Network tab, click on the first document request. Look at Response Headers. All six headers should be present.
Option 3: curl
curl -I https://yourdomain.com | grep -iE "csp|content-security|strict-transport|x-frame|x-content|referrer|permissions"You should see all six headers in the response.
That is it
Six headers, one config file, one deploy. The fix takes longer to read about than to implement. Most Next.js sites in production are missing at least three of these. Yours does not have to be.
"CSP errors are silent in production. The page renders, but a script that violated the policy was blocked, and your app silently fails."
If you want the audit version of "did I do this right" — run Lintry against your domain. The whole point of having a website QA tool is to not have to manually verify every header on every deploy. Set it once, run scans on every release, and stop worrying about regressions.
Get a full report on missing or misconfigured headers in 60 seconds.
Run a free scan →