How to Add Security Headers to Your Next.js App
Why Security Headers Matter
Security headers are HTTP response headers that tell the browser how to handle your page. Without them, your app is vulnerable to:
- XSS (Cross-Site Scripting) — attackers inject scripts that steal user data
- Clickjacking — your app is embedded in a hidden iframe to trick users
- MIME sniffing — browsers misinterpret file types and execute malicious content
- Downgrade attacks — traffic is intercepted by forcing HTTP instead of HTTPS
Most AI coding tools don't add any security headers. This is the most common finding in VibeSafe scans — about 70% of scanned apps are missing critical headers.
The Headers You Need
Here's the complete list, in order of importance:
1. Strict-Transport-Security (HSTS)
Forces browsers to always use HTTPS. Prevents downgrade attacks.
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
2. X-Content-Type-Options
Prevents browsers from MIME-sniffing — stops them from treating a text file as JavaScript.
X-Content-Type-Options: nosniff
3. X-Frame-Options
Prevents your site from being embedded in iframes. Stops clickjacking attacks.
X-Frame-Options: DENY
4. Content-Security-Policy (CSP)
The most powerful security header. Controls which scripts, styles, images, and connections are allowed.
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' https://*.supabase.co
5. Referrer-Policy
Controls how much URL information is sent when navigating to another site.
Referrer-Policy: strict-origin-when-cross-origin
6. Permissions-Policy
Disables browser features you don't use (camera, microphone, geolocation).
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(self)
Adding Headers in Next.js
Method 1: next.config.ts (Recommended)
// next.config.ts
const securityHeaders = [
{ key: "Strict-Transport-Security", value: "max-age=63072000; includeSubDomains; preload" },
{ key: "X-Frame-Options", value: "DENY" },
{ key: "X-Content-Type-Options", value: "nosniff" },
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
{ key: "Permissions-Policy", value: "camera=(), microphone=(), geolocation=(), payment=(self)" },
];
const nextConfig = {
async headers() {
return [{ source: "/(.*)", headers: securityHeaders }];
},
};
export default nextConfig;
Method 2: Middleware (For Dynamic CSP)
Use middleware when you need nonce-based CSP for inline scripts:
// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import crypto from "crypto";
export function middleware(req: NextRequest) {
const nonce = crypto.randomBytes(16).toString("base64");
const res = NextResponse.next();
res.headers.set(
"Content-Security-Policy",
`default-src 'self'; script-src 'self' 'nonce-${nonce}'; style-src 'self' 'unsafe-inline';`
);
return res;
}
Verifying Your Headers
After deploying, verify your headers are working:
- Browser DevTools — Network tab → click any request → check Response Headers
- curl —
curl -I https://yourapp.com - VibeSafe — Scan your URL for a full security header audit
Common Mistakes
Using unsafe-inline in CSP
AI tools often set script-src 'unsafe-inline' which defeats most of CSP's protection. Use nonce-based CSP instead when possible.
Setting HSTS max-age too low
A max-age of 300 (5 minutes) provides almost no protection. Use at least 31536000 (1 year), ideally 63072000 (2 years).
Forgetting about subdomains
If your app uses subdomains (api.yourapp.com, cdn.yourapp.com), add includeSubDomains to your HSTS header.
Testing Header Grade
After adding headers, run a VibeSafe scan to verify everything is configured correctly. Missing or misconfigured headers account for most "medium" severity findings in our reports.