← Blog
·13 min read

Next.js Security Headers — Complete Config | Data Hogo

Next.js security headers are absent by default — Vercel won't add them either. Get the complete next.config.ts block for CSP, HSTS, and 5 more. Free scan.

Rod

Founder & Developer

Next.js ships with zero security headers. create-next-app gives you a working application with routing, TypeScript, and Tailwind — but no Content-Security-Policy, no Strict-Transport-Security, no X-Frame-Options. Nothing. Configuring your Next.js security headers is entirely on you.

Deploying to Vercel doesn't change that. Vercel's free and Pro plans don't set HTTP security headers automatically. You can verify this right now: run curl -I against any fresh Next.js app on Vercel and you'll see x-vercel-id and cache-control in the response — not a single security header.

This isn't a criticism of Next.js or Vercel. They leave the configuration to you because every app's requirements are different. But the practical effect is that most Next.js apps — including ones generated by AI tools like Cursor and v0 — ship without these headers. According to Veracode's State of Software Security 2025 report, 45% of AI-generated code has at least one vulnerability, and missing security headers are among the most consistent findings we see. This connects directly to the AI-generated code and security gaps pattern that shows up across every stack.

The good news: fixing this takes about 20 lines of config. Here's everything you need.


Next.js Ships with Zero Security Headers — Here's What That Means

When a browser loads your Next.js app, it makes an HTTP request and your server sends back a response. That response includes headers — metadata that tells the browser how to handle the content. Some headers are set automatically (Content-Type, Content-Length). Security headers are not.

Without them, your app is missing several layers of browser-enforced protection:

  • No policy restricting which scripts can run (XSS attacks are easier)
  • No instruction to always use HTTPS (protocol downgrade attacks are possible)
  • No rule preventing your page from loading inside an iframe (clickjacking is possible)
  • No guard against MIME-type confusion attacks

Missing HTTP security headers falls under OWASP A05:2021 — Security Misconfiguration, which is #5 in the OWASP Top 10. It's not a dramatic vulnerability. It's a configuration you didn't make. But the consequences are real when someone finds an XSS vector in your app and there's no CSP to contain the damage.

The other thing most Next.js developers don't realize: this applies to your own next.config.ts too. At Data Hogo, when we launched our own app, the initial next.config.ts didn't have a complete security headers block. We know this problem firsthand — which is exactly why we built header scanning into our URL scanner. Our scanner runs checks for all 8 header-related vulnerabilities (Catalog IDs 63–70) on every deployed URL we scan, and missing CSP and missing HSTS are the two most commonly flagged findings in real Next.js app scans.

Want to see which headers your deployed app is missing right now? Check your security headers free — paste your URL and get your score in seconds.


How Security Headers Work in Next.js

You set them in next.config.ts using the headers() function. This is a standard async function that returns an array of route-header mappings. Headers defined here apply to every matching response from your app, including page routes and API routes.

Here's the skeleton structure:

// next.config.ts
import type { NextConfig } from "next";
 
const nextConfig: NextConfig = {
  async headers() {
    return [
      {
        // Apply to all routes
        source: "/(.*)",
        headers: [
          {
            key: "X-Content-Type-Options",
            value: "nosniff",
          },
          // ... more headers
        ],
      },
    ];
  },
};
 
export default nextConfig;

Two things to understand before you start adding headers:

The source pattern is a glob that matches routes. "/(.*)" matches everything. You can apply different headers to different paths — for example, stricter CSP on /admin/(.*) — but for security headers, you almost always want them on everything.

Middleware can override these headers. A src/middleware.ts file can set or modify response headers for specific requests. This matters a lot for Content-Security-Policy with nonces, which we'll cover in its own section. For all the other headers in this post, next.config.ts is the right place.

See the Next.js headers() configuration reference for the full API, including has and missing conditions if you need conditional header logic.


The 7 Next.js Security Headers, Prioritized by Impact

Not all headers are equally important. Missing CSP on an app that renders user content is a serious gap. Missing X-Content-Type-Options is a real finding but not an emergency. Here's the full list ordered by impact.

The OWASP Secure Headers Project maintains a reference for recommended values across all major headers. What follows is the Next.js-specific interpretation.

Header Protection Severity if Missing Vuln ID
Content-Security-Policy XSS, script injection Medium (High for user-content apps) 63
Strict-Transport-Security Protocol downgrade, MITM Medium 66
X-Frame-Options Clickjacking Medium 64
X-Content-Type-Options MIME sniffing attacks Low 65
Referrer-Policy URL data leakage to third parties Low–Medium
Permissions-Policy Unauthorized browser feature access Medium
Cookie security flags Cookie theft, CSRF Medium (per flag) 67, 68, 69

Want to skip the manual audit? Check your headers free with our Security Header Checker — instant results, no signup.

Content-Security-Policy — Severity: Medium (High in Practice)

What it does: CSP tells the browser which scripts, styles, and resources are allowed to load on your page. It's your primary defense against cross-site scripting (XSS) — the attack where an injected script runs in your users' browsers under your domain's identity.

What missing it means: If an attacker finds an XSS vector in your app — a reflected query param, an unsanitized user-generated field — and there's no CSP, that script runs with full access to your DOM, your cookies, and your localStorage. Missing CSP is rated Medium in the Data Hogo catalog, but for apps that render user content, the practical risk is High.

The broken approach (don't do this):

// BAD: This is functionally no CSP — it creates false confidence
// 'unsafe-inline' and 'unsafe-eval' defeat the entire purpose
{
  key: "Content-Security-Policy",
  value: "script-src 'self' 'unsafe-inline' 'unsafe-eval'",
}

A starter policy for Pages Router or simple apps:

// GOOD: Starter CSP — adjust script-src based on your analytics/fonts
{
  key: "Content-Security-Policy",
  value: [
    "default-src 'self'",
    "script-src 'self'",
    "style-src 'self' 'unsafe-inline'",   // Next.js needs unsafe-inline for styles
    "img-src 'self' data: https:",
    "font-src 'self'",
    "object-src 'none'",                  // block Flash, old plugins
    "base-uri 'self'",                    // prevent base tag injection
    "frame-ancestors 'none'",             // blocks iframe embedding
  ].join("; "),
}

Important: If you're using Next.js App Router, a static script-src 'self' policy will break your app. Next.js App Router injects inline scripts for hydration, and those scripts fail a strict script-src 'self' check. You need nonces — covered in the next section.

See the Next.js Content Security Policy documentation for the official guidance on this.

Strict-Transport-Security (HSTS) — Severity: Medium

What it does: HSTS tells browsers to always connect to your site over HTTPS, even if the user types http:// — it prevents protocol downgrade attacks where a network attacker intercepts a non-HTTPS connection before the redirect happens.

// GOOD: One year max-age, applies to all subdomains
// Only set this in production — it persists in the browser
{
  key: "Strict-Transport-Security",
  value: "max-age=31536000; includeSubDomains; preload",
}

Two caveats. First: preload submits your domain to browser HSTS preload lists — browsers will refuse HTTP connections to your domain even before seeing this header. Don't add preload unless every subdomain is HTTPS-only. Check the HSTS preload list requirements before enabling it.

Second: don't set HSTS in development. It persists in the browser and will break http://localhost. Gate it on NODE_ENV:

// Only set HSTS in production
...(process.env.NODE_ENV === "production"
  ? [{ key: "Strict-Transport-Security", value: "max-age=31536000; includeSubDomains; preload" }]
  : [])

X-Frame-Options — Severity: Medium

What it does: Prevents your pages from being embedded in iframes on other domains — the attack vector for clickjacking, where an attacker layers an invisible version of your app over a decoy page to trick users into clicking things they didn't intend to.

// GOOD: DENY blocks all iframe embedding
// Use SAMEORIGIN if you embed your own pages in your own iframes
{
  key: "X-Frame-Options",
  value: "DENY",
}

If you've set frame-ancestors 'none' in your CSP, modern browsers will respect that and ignore X-Frame-Options. Set both anyway — X-Frame-Options is the fallback for older browsers that don't understand CSP.

X-Content-Type-Options — Severity: Low

What it does: Stops browsers from MIME-sniffing — guessing the content type of a response based on its content rather than its declared Content-Type. Without it, a browser might execute an uploaded file as JavaScript if it looks like code.

// GOOD: nosniff is the only valid value — no tradeoffs here
{
  key: "X-Content-Type-Options",
  value: "nosniff",
}

This is the simplest header in the list. One value, no configuration decisions, no edge cases. Add it and move on.

Referrer-Policy — Severity: Low–Medium

What it does: Controls how much of your URL is included in the Referer header when a user clicks a link to another site. Without a policy, browsers default to sending the full URL — including query strings that might contain user IDs, session tokens, or search terms — to every external domain your links point to.

// GOOD: Sends origin only for cross-origin requests, full URL for same-origin
{
  key: "Referrer-Policy",
  value: "strict-origin-when-cross-origin",
}

If your app has URLs like /dashboard?user=12345&token=abc, and a user clicks an external link from that page, a browser without this policy sends that entire URL in the Referer header to the external site. strict-origin-when-cross-origin sends only https://your-domain.com instead.

Permissions-Policy — Severity: Medium

What it does: Controls which browser APIs — camera, microphone, geolocation, payment, and others — your page and its embedded iframes can access. Most Next.js apps don't use any of these. A restrictive policy limits the damage if an attacker injects a malicious third-party script that tries to access them.

// GOOD: Disable everything you don't use
// Add back only what your app needs
{
  key: "Permissions-Policy",
  value: "camera=(), microphone=(), geolocation=(), payment=(), usb=(), interest-cohort=()",
}

interest-cohort=() was the opt-out directive for Google's deprecated FLoC experiment. Include it for completeness — the Topics API replacement has its own separate opt-out mechanism.

Cookie Security Flags — Severity: Medium (per flag)

What they do: Three flags belong on every Set-Cookie response your app sends:

  • HttpOnly — prevents JavaScript from reading the cookie, blocking cookie theft via XSS
  • Secure — only sends the cookie over HTTPS connections
  • SameSite=Lax or SameSite=Strict — restricts cross-site cookie sending for CSRF protection

Important distinction: Cookie flags go in your API routes and Server Actions — not in next.config.ts. The headers() function can't set Set-Cookie in a useful way because cookie values are dynamic. You set them when you call cookies().set():

// src/app/api/auth/route.ts
import { cookies } from "next/headers";
 
export async function POST(req: Request) {
  const { token } = await req.json();
  const cookieStore = await cookies();
 
  cookieStore.set("session", token, {
    httpOnly: true,                                          // blocks JS access
    secure: process.env.NODE_ENV === "production",          // HTTPS only in prod
    sameSite: "lax",                                        // CSRF protection
    path: "/",
    maxAge: 60 * 60 * 24 * 7,                               // 7 days
  });
}

The secure: process.env.NODE_ENV === "production" pattern is important. secure: true hardcoded will break local development over http://localhost — the cookie won't be sent at all, and you'll spend 20 minutes debugging why your auth doesn't work locally. Always gate it on NODE_ENV.


CSP in Next.js App Router: The Nonce Approach

This is where most CSP guides fall apart. A static Content-Security-Policy header with script-src 'self' will break a Next.js App Router application. The reason: App Router injects inline <script> tags for hydration — the client-side data transfer that makes Server Components work. Those inline scripts don't come from 'self' (your domain), so a strict script-src 'self' policy blocks them and your app breaks.

The correct approach for App Router is nonce-based CSP. A nonce is a random value generated fresh for every request. You include it in both the Content-Security-Policy header and in the nonce attribute on your inline scripts. The browser trusts any inline script that has the correct nonce — and since the nonce is random per request, an attacker can't predict it.

This requires middleware — not next.config.ts — because the nonce must be generated per request. Here's the complete implementation.

Step 1: Generate the nonce and set the CSP header in middleware

// src/middleware.ts
import { NextRequest, NextResponse } from "next/server";
 
export function middleware(request: NextRequest) {
  // Generate a fresh nonce for every request
  const nonce = Buffer.from(crypto.randomUUID()).toString("base64");
 
  const csp = [
    "default-src 'self'",
    `script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`,  // strict-dynamic lets nonce'd scripts load others
    "style-src 'self' 'unsafe-inline'",                      // Next.js needs unsafe-inline for styles
    "img-src 'self' data: https:",
    "font-src 'self'",
    "object-src 'none'",
    "base-uri 'self'",
    "frame-ancestors 'none'",
  ].join("; ");
 
  const requestHeaders = new Headers(request.headers);
  // Pass nonce to the layout via a request header
  requestHeaders.set("x-nonce", nonce);
 
  const response = NextResponse.next({
    request: { headers: requestHeaders },
  });
 
  // Set the CSP on the actual response
  response.headers.set("Content-Security-Policy", csp);
 
  return response;
}
 
export const config = {
  matcher: [
    // Match all routes except static files and Next.js internals
    "/((?!_next/static|_next/image|favicon.ico).*)",
  ],
};

Step 2: Read the nonce in your root layout and pass it to Next.js

// src/app/[locale]/layout.tsx
import { headers } from "next/headers";
import Script from "next/script";
 
export default async function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  // Read the nonce that middleware set on the request
  const headersList = await headers();
  const nonce = headersList.get("x-nonce") ?? "";
 
  return (
    <html>
      <body>
        {children}
        {/* Pass nonce to any inline scripts you add manually */}
        <Script
          id="analytics-init"
          nonce={nonce}
          strategy="afterInteractive"
          dangerouslySetInnerHTML={{ __html: `/* your inline script */` }}
        />
      </body>
    </html>
  );
}

Next.js automatically reads the x-nonce value that middleware sets on the request headers and applies it to its own inline hydration scripts. You don't need to manually attach the nonce to anything for Next.js's scripts — the middleware handles that. You do need to pass nonce explicitly to any third-party <Script> tags you add yourself, like the analytics example above.

Do not set CSP in next.config.ts when using the nonce approach. The next.config.ts headers are static — they can't contain a per-request nonce. If you set CSP in both places, the static header will override the dynamic one (or conflict with it, depending on your CDN). The middleware sets the CSP. next.config.ts sets everything else.

For a deeper reference, see Content-Security-Policy on MDN for the full directive specification.


Verify Your Headers Are Being Sent

Adding headers to next.config.ts doesn't guarantee they're being sent. A middleware error, a CDN override, or a config mistake can silently prevent them. Here are three ways to verify.

Option 1: curl in your terminal

# Replace with your actual deployed URL
curl -I https://your-app.vercel.app

You should see your security headers in the output. If you don't see content-security-policy, strict-transport-security, or x-frame-options, something in your config isn't working.

Option 2: Browser DevTools

Open DevTools, go to the Network tab, reload your page, click the first request (your document), and look at the Response Headers panel. Every header your server sends is listed there.

Option 3: Mozilla Observatory

Go to Mozilla Observatory and run your domain through it. Observatory grades your headers, shows you exactly what's missing, and explains what each missing header means. It's free and gives you a letter grade — useful for a quick sanity check.

For automated verification on every deploy, Data Hogo's URL scanner hits your deployed URL and checks all 8 header-related vulnerabilities. Instead of running curl manually after every deployment, you get a finding list with plain-language descriptions of what's missing and what to fix. The free plan covers 3 scans per month.

If you want to go deeper than headers and scan your entire codebase for security gaps, check out scanning your Cursor-generated code as a companion guide. You can also check if your .env file is publicly accessible — and if you've already committed secrets, the exposed API key fix guide covers the full recovery process.

Check your security headers now — paste your URL and see which headers are missing, misconfigured, or correctly set.


The Complete next.config.ts Security Headers Block

Here's the full copy-paste config. Drop it into your next.config.ts and it works for a standard Next.js + Vercel deployment.

Note on CSP: This block includes a fallback CSP for Pages Router apps or any app that doesn't use the nonce approach. If you're on App Router, replace this with the middleware approach from the previous section and don't set CSP here.

// next.config.ts
import type { NextConfig } from "next";
 
const nextConfig: NextConfig = {
  async headers() {
    return [
      {
        source: "/(.*)",
        headers: [
          // ─── Content Security Policy ─────────────────────────────────
          // App Router: remove this and use the nonce middleware instead
          // Pages Router: use this starter policy, add your CDN/analytics domains
          {
            key: "Content-Security-Policy",
            value: [
              "default-src 'self'",
              "script-src 'self'",
              "style-src 'self' 'unsafe-inline'",
              "img-src 'self' data: https:",
              "font-src 'self'",
              "object-src 'none'",
              "base-uri 'self'",
              "frame-ancestors 'none'",
            ].join("; "),
          },
 
          // ─── HSTS ────────────────────────────────────────────────────
          // Only active in production — remove the conditional if you
          // are certain every subdomain is HTTPS-only and want preloading
          ...(process.env.NODE_ENV === "production"
            ? [
                {
                  key: "Strict-Transport-Security",
                  value: "max-age=31536000; includeSubDomains; preload",
                },
              ]
            : []),
 
          // ─── Clickjacking protection ─────────────────────────────────
          // Also set frame-ancestors in CSP for modern browser support
          {
            key: "X-Frame-Options",
            value: "DENY",
          },
 
          // ─── MIME sniffing ───────────────────────────────────────────
          // nosniff is the only valid value — no tradeoffs
          {
            key: "X-Content-Type-Options",
            value: "nosniff",
          },
 
          // ─── Referrer leakage ────────────────────────────────────────
          // Sends origin only on cross-origin requests (not full URL)
          {
            key: "Referrer-Policy",
            value: "strict-origin-when-cross-origin",
          },
 
          // ─── Browser feature access ──────────────────────────────────
          // Add back camera=self, microphone=self, etc. if your app uses them
          {
            key: "Permissions-Policy",
            value: [
              "camera=()",
              "microphone=()",
              "geolocation=()",
              "payment=()",
              "usb=()",
              "interest-cohort=()",
            ].join(", "),
          },
        ],
      },
    ];
  },
};
 
export default nextConfig;

A few places you might need to adjust this for your specific app:

  • script-src: Add any CDN domains for external scripts you load (e.g., 'https://cdn.posthog.com' if you use PostHog's hosted CDN)
  • img-src: Add image CDN domains if you use Cloudinary, Imgix, or similar
  • font-src: Add https://fonts.gstatic.com if you use Google Fonts
  • Permissions-Policy: Add back camera=self or geolocation=self if your app uses those APIs

Security headers are one defense layer. Your database authorization is another — if you're building with Supabase, the Supabase RLS security checklist covers the database side of the same threat model.


Frequently Asked Questions

How do I add security headers in Next.js?

Add a headers() function to your next.config.ts that returns an array of header objects. Each object has a source pattern (the route to match) and a headers array of key-value pairs. Headers defined here apply to every matching response, including page routes and API routes. For CSP with nonces in the App Router, you'll also need src/middleware.ts to generate a per-request nonce.

Does Next.js add security headers automatically?

No. Next.js ships with zero security headers out of the box. Running create-next-app gives you a working application with no Content-Security-Policy, no Strict-Transport-Security, no X-Frame-Options — nothing. You have to opt in explicitly by configuring the headers() function in next.config.ts.

What is the difference between X-Frame-Options and CSP frame-ancestors?

Both headers prevent your page from being embedded in an iframe on another site. X-Frame-Options is the older standard and is supported by all browsers including legacy ones. CSP frame-ancestors is the modern replacement with more granular control — you can allow specific origins rather than only same-origin or none. Best practice is to set both: frame-ancestors for modern browsers and X-Frame-Options as a fallback for older ones.

Do I need Helmet.js in a Next.js app?

No. Helmet.js is an Express.js middleware — it doesn't work with Next.js because Next.js isn't built on Express. The equivalent in Next.js is the headers() function in next.config.ts, which sets response headers globally across all your routes. For dynamic headers like CSP nonces, you use Next.js middleware in src/middleware.ts instead.

How do I test if my Next.js app has security headers?

Three ways: run curl -I https://your-domain.com in your terminal and check the response headers, open browser DevTools and look at the Response Headers for your document request, or run your URL through Mozilla Observatory for a graded report. To check automatically on every deploy, Data Hogo's URL scanner hits your deployed URL and flags any missing or misconfigured headers as part of a full security scan.

Does Vercel add security headers for me?

No, not automatically. Vercel's free and Pro plans don't add security headers to your Next.js app by default. You own this configuration. You can verify this yourself: deploy a fresh Next.js app to Vercel and run curl -I against the URL — you'll see Vercel's own headers (x-vercel-id, cache-control) but no Content-Security-Policy or Strict-Transport-Security. Vercel's Enterprise plan offers configurable security headers as a platform feature, but that's a separate product tier with its own configuration.


Adding Next.js security headers takes 20 minutes and pays off permanently. Add the config block, deploy, verify with curl, and move on to the common vulnerabilities in AI-generated Next.js apps if you want to cover the rest of the security surface.

Check your security headers now — free, instant, no signup required.

Next.jssecurity headersCSPHSTSVercelweb securityvibe-coding