How to set up security headers in Next.js
HTTP security headers are instructions your server sends to the browser along with every response. They tell the browser how to behave when rendering your page: whether it can be embedded in an iframe, which external scripts it is allowed to load, whether it should upgrade HTTP connections to HTTPS automatically, and how it should handle content type mismatches. These instructions are not enforced by your server — they are enforced by the browser — which means they only protect users whose browsers respect them (which is essentially all modern browsers). Without them, the browser uses permissive defaults that leave your users open to clickjacking, cross-site scripting escalation, and downgrade attacks. Adding security headers to a Next.js app takes less than thirty minutes and requires no external dependencies.
The five headers every Next.js app needs are: X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Strict-Transport-Security, and Content-Security-Policy. X-Frame-Options set to DENY prevents your page from being embedded in an iframe on a third-party site, which is the foundation of clickjacking attacks where an attacker overlays your interface on a malicious page to capture clicks. X-Content-Type-Options set to nosniff prevents the browser from guessing the content type of a response and executing it as something it is not — a common escalation technique when a site allows file uploads. Referrer-Policy set to strict-origin-when-cross-origin controls what URL information leaks when a user navigates from your site to a third-party page. Strict-Transport-Security with a long max-age and includeSubDomains tells browsers to never connect to your domain over plain HTTP, preventing downgrade attacks on public Wi-Fi networks.
Adding these headers in Next.js is straightforward using the headers() function in next.config.ts. Add a configuration block that applies headers to every route using the source: "/(.*) " pattern:
async headers() {
return [
{
source: "/(.*)",
headers: [
{ key: "X-Frame-Options", value: "DENY" },
{ key: "X-Content-Type-Options", value: "nosniff" },
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
{
key: "Strict-Transport-Security",
value: "max-age=31536000; includeSubDomains",
},
{ key: "Permissions-Policy", value: "camera=(), microphone=(), geolocation=()" },
],
},
];
}Deploy this change and verify it worked by running curl -I https://yourdomain.com — each header should appear in the response.
The Content Security Policy header deserves separate attention because it is the most powerful and the most complex. A CSP tells the browser which origins are allowed to provide scripts, styles, images, fonts, and other resources for your page. A strict CSP that blocks all inline scripts and limits external script sources to a whitelist significantly reduces the impact of any XSS vulnerability — even if an attacker manages to inject a script tag, the browser will refuse to execute it if the source does not match your policy. For Next.js apps, the recommended approach is a nonce-based CSP: the server generates a unique random nonce for each request, passes it to the page, and stamps it on every script tag. The CSP then allows scripts with 'nonce-{nonce}' and uses 'strict-dynamic' to propagate trust to scripts loaded by trusted ones. This is more complex to implement than static headers but provides meaningfully stronger protection.
After deploying your headers, run a scanner against your live site to confirm all headers are present and correctly configured. Common mistakes to watch for: headers added in development but not production due to environment-specific config; middleware that overwrites headers set in next.config.ts; and CSP directives that are too broad because you started with 'unsafe-inline'to make things work and never tightened it. A well-configured set of security headers will change your security scanner's assessment from multiple high-severity findings to a clean pass on the entire headers category.