All posts

Field guide

How to add a Content Security Policy to a Next.js app

A Content Security Policy is the header that tells the browser which scripts to trust, so an injected one never runs. Most AI-built apps ship without it, which means an XSS bug has nothing standing in its way. Here is how to add one to Next.js without breaking your app.

Last updated

A Content Security Policy is one of the highest-value headers you can add, because it turns a cross-site scripting bug from a full compromise into a non-event: the browser refuses to run script from anywhere you did not approve. Most generated apps ship without one. Adding it is straightforward; the only real work is making sure your own scripts still run.

The simple version: a static policy in the config

If your app has no inline scripts to worry about, you can set the header in next.config.js for every route:

// next.config.js
const csp = [
  "default-src 'self'",
  "script-src 'self'",
  "style-src 'self' 'unsafe-inline'",   // many apps need inline styles
  "img-src 'self' data: https:",
  "frame-ancestors 'none'",             // also blocks clickjacking
  "base-uri 'self'",
].join("; ");

module.exports = {
  async headers() {
    return [{
      source: "/:path*",
      headers: [{ key: "Content-Security-Policy", value: csp }],
    }];
  },
};

Note frame-ancestors 'none', which also stops clickjacking, and that this is one of the security headers worth having together.

Want to see exactly what Prbl flags? Watch it scan a demo app, no repo or account needed.

See a live scan →

The robust version: a nonce in middleware

If you have legitimate inline scripts, avoid unsafe-inline for script and use a per-request nonce instead. Generate it in middleware, add it to the CSP, and Next.js applies it to its own scripts:

// middleware.ts
import { NextResponse } from "next/server";

export function middleware(request: Request) {
  const nonce = crypto.randomUUID().replace(/-/g, "");
  const csp = [
    "default-src 'self'",
    `script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`,
    "style-src 'self' 'unsafe-inline'",
    "frame-ancestors 'none'",
    "base-uri 'self'",
  ].join("; ");

  const headers = new Headers(request.headers);
  headers.set("x-nonce", nonce);
  const res = NextResponse.next({ request: { headers } });
  res.headers.set("Content-Security-Policy", csp);
  return res;
}

Only scripts carrying the matching nonce run, so your own inline scripts work and an injected one, which cannot know the nonce, does not.

On an existing app, start in report-only

Retrofitting a strict CSP can block things you forgot about. Ship it as Content-Security-Policy-Report-Only first, which logs violations without blocking, watch what it would break over real traffic, fix or allow those cases, then switch to the enforcing header. On a new app, design the policy in from the start and enforce right away.

Frequently asked questions

What does a Content Security Policy actually do?

It is an HTTP header that tells the browser which sources of scripts, styles, and other content it is allowed to load and run. By allowing only trusted origins and blocking inline script by default, a good CSP means an injected script simply will not execute. It does not fix a cross-site scripting bug, but it removes the bug's payoff, which is a large part of the damage.

Why is it hard to add later?

Because a strict policy can break inline scripts, third-party widgets, and analytics that a loose app relies on. Retrofitting means finding everything that would be blocked and either allowing it explicitly or reworking it. That is why report-only mode exists: it logs what would be blocked without actually blocking, so you can tighten the policy against real traffic before you enforce it.

What is a nonce and do I need one?

A nonce is a random value you put on a script tag and in the CSP header for a single request, telling the browser to trust exactly that inline script and nothing else. You need it if you have legitimate inline scripts and want to avoid unsafe-inline, which weakens the policy. Next.js supports generating a nonce in middleware and passing it through, which is the clean way to allow your own inline scripts without opening the door to injected ones.

Should I start with report-only?

Yes, on an existing app. Ship Content-Security-Policy-Report-Only first, watch what it would block over real usage, fix or allow those cases, then switch to the enforcing header. On a brand-new app you can design the policy in from the start and enforce immediately, which is much easier than retrofitting.

Check what headers your app sends

A CSP is most valuable alongside the other protections, and it pairs with fixing the XSS it is meant to contain. A scan reads what your deployed app serves and flags missing protections. Run a free scan and see what your app is shipping.

Prbl scans your live app or your codebase for exactly the kinds of issues above.

How to Add a Content Security Policy to a Next.js App: Prbl