# Content Security Policy (CSP): A Practical Guide with Nonces

Cross-site scripting works by getting the browser to run JavaScript you didn't write. A Content Security Policy is your allowlist: the browser refuses any script, style, frame or connection that the policy doesn't permit. Done well, it turns most XSS bugs into a harmless blocked request. Done badly — with `'unsafe-inline'` sprinkled in to make errors go away — it protects almost nothing.

## How a policy reads

A policy is a list of directives separated by semicolons. Each names a resource type and the sources allowed for it:

```http
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com; img-src 'self' data:; object-src 'none'
```

- `default-src` is the fallback for fetch directives you don't list.
- `'self'` means your own origin — scheme, host and port.
- Quoted keywords (`'self'`, `'none'`, `'unsafe-inline'`) need the single quotes; host sources don't.

The directives that matter most: `script-src`, `style-src`, `img-src`, `connect-src` (fetch, XHR and WebSockets), `frame-src` (what you embed), `frame-ancestors` (who may embed you), `object-src`, `base-uri` and `form-action`. The [Content-Security-Policy reference](/headers/content-security-policy) lists them all.

## Step 1: report before you block

Send the policy as `Content-Security-Policy-Report-Only`. The browser evaluates it and logs every violation in the console, but blocks nothing:

```http
Content-Security-Policy-Report-Only: default-src 'self'; object-src 'none'; base-uri 'self'
```

Browse your site, fix or allow what shows up, then switch the header name to `Content-Security-Policy`. To collect violations from real users, add a reporting endpoint:

```http
Reporting-Endpoints: csp="https://example.com/csp-reports"
Content-Security-Policy-Report-Only: default-src 'self'; report-to csp; report-uri https://example.com/csp-reports
```

`report-to` is the current mechanism; `report-uri` is deprecated but still the one some browsers use, so sending both is common.

## Step 2: deal with inline scripts

Inline `<script>` blocks and `onclick="…"` attributes are exactly what injected code looks like, so a policy without `'unsafe-inline'` blocks them all. Allowing `'unsafe-inline'` makes the policy almost useless against XSS. There are two proper fixes.

**Nonces.** Generate a random value for every response, put it in the policy, and add it to each script tag you trust:

```http
Content-Security-Policy: script-src 'nonce-r4nd0mBase64Value' 'strict-dynamic'; object-src 'none'; base-uri 'none'
```

```html
<script nonce="r4nd0mBase64Value" src="/app.js"></script>
```

An attacker who injects a `<script>` can't know the nonce, so their script doesn't run. The nonce must be unpredictable and **new on every response** — a fixed nonce is no better than `'unsafe-inline'`.

**Hashes.** For a static inline script that never changes, allow its exact contents by hash instead. For `<script>console.log("hello");</script>`:

```http
Content-Security-Policy: script-src 'self' 'sha256-N4H5TqgSuzNDfekEngS8OvQaDnOXFksFc3nAjDsKxIk='
```

The hash covers the text between the tags exactly, whitespace included — change one character and it stops matching.

You don't need to compute the hash by hand: when Chrome blocks the script, the console error includes the exact `'sha256-…'` value to add.

Inline event handlers (`onclick`, `onload`) and `javascript:` URLs can't carry nonces. Move them into script files with `addEventListener`.

## strict-dynamic

Modern bundles load more scripts at runtime, and analytics tags load further scripts of their own. Listing every host is brittle. `'strict-dynamic'` says: trust scripts that a nonced or hashed script loads. Browsers that support it ignore host allowlists and `'self'` in `script-src`, which is why a nonce plus `'strict-dynamic'` is the policy Google's security team recommends.

## Nonces in Next.js

Next.js reads the nonce from the request's CSP header and applies it to its own scripts. Generate it in middleware (called `proxy.ts` from Next.js 16):

```ts
import { NextResponse, type NextRequest } from "next/server";

export function middleware(request: NextRequest) {
  const nonce = Buffer.from(crypto.randomUUID()).toString("base64");
  const csp = [
    "default-src 'self'",
    `script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`,
    "img-src 'self' blob: data:",
    "object-src 'none'",
    "base-uri 'self'",
    "frame-ancestors 'none'",
  ].join("; ");

  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;
}
```

The catch: a per-request nonce means pages can't be served as static HTML, since each response needs a fresh value. For mostly static sites, a hash-based or host-based policy may be the better trade.

## What usually breaks

| Symptom | Cause | Fix |
|---|---|---|
| Analytics or chat widget stops working | Its script, API and image hosts aren't allowed | Add hosts to `script-src`, `connect-src`, `img-src` — or use a nonce with `'strict-dynamic'` |
| "Refused to evaluate a string as JavaScript" | `eval()` or `new Function()`, often in older libraries or dev builds | Replace the code; `'unsafe-eval'` only as a last resort |
| Inline styles ignored | `style-src` without `'unsafe-inline'` blocks `style=""` attributes | Nonce style tags, or allow `'unsafe-inline'` for styles only — far less dangerous than for scripts |
| Fonts or images missing | `font-src` / `img-src` falls back to `default-src` | Add the CDN host, or `data:` for inline images |
| Site can't be embedded where you want it | `frame-ancestors 'none'` | List the allowed parent origins |

## Checklist

- Start with `-Report-Only`; enforce once the console is quiet.
- No `'unsafe-inline'` in `script-src` — use nonces or hashes.
- Always include `object-src 'none'` and `base-uri 'self'` (or `'none'`).
- Add `frame-ancestors` to replace [X-Frame-Options](/headers/x-frame-options).
- Re-test after adding any third-party script.

CSP is one of six headers worth sending on every site — see the [security headers checklist](/blog/security-headers-checklist) for the others.
