Missing Security Headers in Next.js
Next.js does not set security headers by default. Without explicit configuration in next.config.js or middleware, your application ships without Content-Security-Policy, Strict-Transport-Security, X-Content-Type-Options, and other headers that protect against XSS, clickjacking, and protocol downgrade attacks.
Scan Your Next.js AppHow Missing Security Headers Manifests in Next.js
When you deploy a Next.js application without configuring security headers, browsers receive no instructions about content restrictions. This manifests in several ways: No Content-Security-Policy means inline scripts, eval(), and third-party scripts can execute freely. An XSS vulnerability becomes trivially exploitable because there is no CSP to block injected scripts. No Strict-Transport-Security means browsers will not enforce HTTPS on subsequent visits. Even if your hosting platform redirects HTTP to HTTPS, the first request can be intercepted in a man-in-the-middle attack. Missing X-Content-Type-Options allows MIME-type sniffing, where browsers reinterpret uploaded files (like an HTML file uploaded as .txt) as executable content. Missing X-Frame-Options or a permissive frame-ancestors CSP directive leaves the app vulnerable to clickjacking. Next.js Middleware and the headers() function in next.config.js are the two configuration points, but developers often overlook both, assuming the framework or hosting platform adds headers automatically.
Real-World Impact
A Next.js e-commerce site was deployed to Vercel without security headers. An attacker found a reflected XSS vulnerability in a search parameter. Without CSP, the injected script ran unrestricted, logging keystrokes on the checkout page and exfiltrating credit card numbers to an external server. Another Next.js app served user-uploaded files without X-Content-Type-Options: nosniff. An attacker uploaded an HTML file disguised as a .jpg image. When other users accessed the file URL, browsers sniffed the content type and rendered it as HTML, executing embedded JavaScript.
Step-by-Step Fix
Add security headers in next.config.js
Use the headers() function to set all essential security headers for every route.
// next.config.js
const securityHeaders = [
{ key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'X-Frame-Options', value: 'DENY' },
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
{ key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubDomains; preload' },
{ key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' },
];
const nextConfig = {
async headers() {
return [
{
source: '/(.*)',
headers: securityHeaders,
},
];
},
};
module.exports = nextConfig;Add nonce-based CSP with Middleware
Generate a per-request nonce and inject it into the CSP header using Next.js Middleware for strong script protection.
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
import crypto from 'crypto';
export function middleware(request: NextRequest) {
const nonce = crypto.randomBytes(16).toString('base64');
const csp = [
`default-src 'self'`,
`script-src 'self' 'nonce-${nonce}'`,
`style-src 'self' 'unsafe-inline'`,
`img-src 'self' data: https:`,
`font-src 'self'`,
`connect-src 'self'`,
`frame-ancestors 'none'`,
].join('; ');
const response = NextResponse.next();
response.headers.set('Content-Security-Policy', csp);
response.headers.set('x-nonce', nonce);
return response;
}
export const config = { matcher: ['/((?!_next/static|favicon.ico).*)'] };Set Permissions-Policy based on your app needs
Configure Permissions-Policy to disable browser features your app does not use. Check if your app needs geolocation, camera, or microphone before blocking them.
// If your app does NOT use geolocation, camera, or microphone:
{ key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' }
// If your app USES geolocation (e.g., maps, location-based features):
{ key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=(self)' }
// Check your app before applying:
// grep -r "navigator.geolocation" --include="*.ts" --include="*.tsx"
// grep -r "getUserMedia" --include="*.ts" --include="*.tsx"Verify headers are applied correctly
Check that headers are present on all responses, including API routes and static assets.
// Test script to verify security headers
// Run: npx ts-node scripts/check-headers.ts
const REQUIRED_HEADERS = [
'content-security-policy',
'strict-transport-security',
'x-content-type-options',
'x-frame-options',
'referrer-policy',
'permissions-policy',
];
async function checkHeaders(url: string) {
const res = await fetch(url);
const missing = REQUIRED_HEADERS.filter(h => !res.headers.get(h));
if (missing.length > 0) {
console.error(`Missing headers on ${url}:`, missing);
} else {
console.log(`All headers present on ${url}`);
}
}
checkHeaders('https://your-app.vercel.app');
checkHeaders('https://your-app.vercel.app/api/health');Prevention Best Practices
1. Configure security headers in next.config.js using the headers() async function for static header values. 2. Use Next.js Middleware for dynamic headers like nonce-based CSP. 3. Always include: Content-Security-Policy, Strict-Transport-Security, X-Content-Type-Options, X-Frame-Options, Referrer-Policy, and Permissions-Policy. 4. Test headers in staging before production to avoid breaking functionality. 5. Review headers after every deployment since misconfigurations can silently revert.
How to Test
1. Open your deployed Next.js app and check response headers in browser DevTools (Network tab). 2. Look for missing Content-Security-Policy, Strict-Transport-Security, X-Content-Type-Options, X-Frame-Options, and Referrer-Policy. 3. Use curl -I https://your-app.com to inspect headers from the command line. 4. Check both page routes and API routes — headers may differ between them. 5. Use Vibe App Scanner to automatically detect missing security headers across all your application's endpoints.
Frequently Asked Questions
Does Vercel add security headers to Next.js apps automatically?
No. Vercel does not add security headers by default. It handles HTTPS and adds some caching headers, but Content-Security-Policy, X-Frame-Options, HSTS, and other security headers must be configured explicitly in your next.config.js or Middleware.
Should I use next.config.js headers() or Middleware for security headers?
Use next.config.js headers() for static values like X-Content-Type-Options: nosniff and Strict-Transport-Security. Use Middleware when you need dynamic values like a per-request CSP nonce. You can combine both approaches — static headers in next.config.js and dynamic CSP in Middleware.
Will adding CSP break my Next.js application?
A strict CSP can break inline scripts, third-party analytics, and CDN-loaded resources. Start with Content-Security-Policy-Report-Only to log violations without blocking them. Review the violation reports, then gradually tighten the policy. Next.js nonce-based CSP avoids breaking inline scripts generated by the framework.
Related Security Resources
Is Your Next.js App Vulnerable to Missing Security Headers?
VAS automatically scans for missing security headers vulnerabilities in Next.js applications and provides step-by-step remediation guidance with code examples.
Scans from $5, results in minutes. Get actionable fixes tailored to your Next.js stack.