Skip to the solution
HardFrontend10 min read

How do you set security headers and a nonce-based Content Security Policy in Next.js?

91 views

The problem

Question presented to candidate: "A security review flagged our Next.js app: no CSP, no HSTS, and it can be framed. Walk me through adding security headers, and a Content Security Policy strict enough that an injected script cannot run."

What a strong answer should cover:

  • Static headers (HSTS, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, framing protection) belong in headers() in next.config.
  • A strict CSP avoids 'unsafe-inline' by using a per-request nonce, generated in proxy.ts and sent on both the request (so Next.js can read it) and the response.
  • Next.js reads the nonce from the request's Content-Security-Policy header during rendering and adds it to its own scripts; your <Script> tags get it via headers().get("x-nonce").
  • Nonces need dynamic rendering: a statically prerendered page has scripts without nonces, which the browser then blocks; this also rules out ISR, CDN caching of HTML and PPR shells.
  • Alternatives and details: CSP without nonces in next.config, experimental SRI (hashes), 'strict-dynamic', 'unsafe-eval' only in development, and a matcher that skips static assets and prefetches.

Clarifying questions expected:

  • "Must pages stay static or cacheable at the CDN?" β€” that decides between nonces and a weaker or hash-based policy.
  • "Which third-party scripts must run?" β€” they need the nonce or an allowlisted origin.

Code / implementation expected: Yes β€” next.config headers plus a proxy.ts that sets a nonce-based CSP.

  • nextjs
  • security
  • csp
  • headers

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

10 min read Β· 9 sections

Read the code

next.config.ts: static security headers for every route
// next.config.ts
import type { NextConfig } from "next";

const securityHeaders = [
  { key: "Strict-Transport-Security", value: "max-age=63072000; includeSubDomains; preload" },
  { key: "X-Content-Type-Options", value: "nosniff" },
  { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
  { key: "Permissions-Policy", value: "camera=(), microphone=(), geolocation=()" },
  { key: "X-Frame-Options", value: "DENY" },
];

const nextConfig: NextConfig = {
  poweredByHeader: false,
  async headers() {
    return [{ source: "/:path*", headers: securityHeaders }];
  },
};
export default nextConfig;
proxy.ts: a fresh nonce and a strict CSP per request
// proxy.ts (project root, next to app/)
import { NextRequest, NextResponse } from "next/server";

export function proxy(request: NextRequest) {
  const nonce = Buffer.from(crypto.randomUUID()).toString("base64");
  const isDev = process.env.NODE_ENV === "development";
  const csp = `
    default-src 'self';
    script-src 'self' 'nonce-${nonce}' 'strict-dynamic'${isDev ? " 'unsafe-eval'" : ""};
    style-src 'self' 'nonce-${nonce}';
    img-src 'self' blob: data:;
    font-src 'self';
    object-src 'none';
    base-uri 'self';
    form-action 'self';
    frame-ancestors 'none';
    upgrade-insecure-requests;
  `.replace(/\s{2,}/g, " ").trim();

  const requestHeaders = new Headers(request.headers);
  requestHeaders.set("x-nonce", nonce);
  requestHeaders.set("Content-Security-Policy", csp);

  const response = NextResponse.next({ request: { headers: requestHeaders } });
  response.headers.set("Content-Security-Policy", csp);
  return response;
}

export const config = {
  matcher: [
    {
      source: "/((?!api|_next/static|_next/image|favicon.ico).*)",
      missing: [
        { type: "header", key: "next-router-prefetch" },
        { type: "header", key: "purpose", value: "prefetch" },
      ],
    },
  ],
};
A dynamic page that passes the nonce to a third-party script
// app/dynamic/page.tsx
import { headers } from "next/headers";
import Script from "next/script";
import Counter from "./Counter";

export default async function Page() {
  const nonce = (await headers()).get("x-nonce") ?? undefined;
  return (
    <main>
      <h1>Dynamic page</h1>
      <Counter />
      <Script src="/analytics.js" strategy="afterInteractive" nonce={nonce} />
    </main>
  );
}

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 126 of 129 in Next.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Next.js questions
Up nextHard

Partial Prerendering (PPR) β€” how do you enable it incrementally?

Continue