# CSP uses nonce or hash-based script allowlisting

- **Pattern:** `ab-002418` (`security-headers-ii.csp-quality.nonce-or-hash-csp`)
- **Severity:** critical
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-002418
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-002418)

## Why it matters

Domain-based CSP allowlists are bypassed by JSONP endpoints and CDN-hosted gadget scripts — any page served from an allowed domain becomes a script injection vector. Attackers exploit this to execute arbitrary JavaScript in your users' browsers, exfiltrating session tokens, credentials, or payment data. CWE-79 (XSS) and CWE-693 (Protection Mechanism Failure) both apply directly. OWASP A03 (Injection) explicitly calls out CSP bypass via allowlisted domains as an unmitigated XSS vector. Nonce or hash-based allowlisting closes this bypass completely because each nonce is request-unique and cannot be predicted or replicated from another origin.

## Severity rationale

Critical because a bypassed CSP provides zero XSS protection — an attacker with a JSONP gadget on any allowlisted CDN achieves full script execution as if no policy existed.

## Remediation

Switch from domain allowlists to per-request nonces in `middleware.ts`. Each nonce is cryptographically random and single-use, making CDN-gadget bypasses impossible.

```ts
// middleware.ts
import { NextResponse } from 'next/server'
import 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'`,
    `style-src 'self' 'nonce-${nonce}'`,
  ].join('; ')
  const response = NextResponse.next()
  response.headers.set('Content-Security-Policy', csp)
  response.headers.set('x-nonce', nonce)
  return response
}
```

Pass the nonce to every `<Script nonce={nonce}>` component. Remove all domain entries from `script-src` — they are no longer needed once `'strict-dynamic'` is set.

## Detection

- **ID:** `nonce-or-hash-csp`
- **Severity:** `critical`
- **What to look for:** Parse the CSP `script-src` directive. Count all sources listed. Classify each source as: nonce-based (`'nonce-...'`), hash-based (`'sha256-...'`, `'sha384-...'`, `'sha512-...'`), `'strict-dynamic'`, keyword (`'self'`, `'unsafe-inline'`, `'unsafe-eval'`), or domain-only (e.g., `cdn.example.com`, `*.example.com`, `https:`). Domain allowlists are bypassable via JSONP endpoints and CDN-hosted gadget scripts. In Next.js, check for nonce propagation in middleware or layout — Next.js has a built-in nonce pattern via `generateCspNonce()`.
- **Pass criteria:** 100% of script-src sources use nonces, hashes, or `'strict-dynamic'` — 0 domain-only allowlists. `'self'` is acceptable alongside nonces/hashes. Report: "X of Y script-src sources use nonces/hashes."
- **Fail criteria:** At least 1 domain-only allowlist entry in script-src (e.g., `script-src cdn.example.com` without an accompanying nonce/hash).
- **Do NOT pass when:** Only `'self'` is present without nonces or hashes — `'self'` alone is better than domain allowlists but does not provide nonce/hash-level protection.
- **Skip (N/A) when:** No Content-Security-Policy header configured. Run Security Headers & Basics first.
- **Cross-reference:** For basic CSP presence, the Security Headers & Basics audit covers this.
- **Detail on fail:** `"X of Y script-src sources are domain-only allowlists — bypassable via JSONP endpoints"` or `"script-src uses domain allowlists (cdn.example.com) without nonce or hash supplementation"`
- **Remediation:** Domain-based CSP allowlists are fundamentally insecure — any page hosted on an allowed domain can serve as a CSP bypass gadget. Switch to nonces:

  ```ts
  // middleware.ts — generate a nonce per request
  import { NextResponse } from 'next/server'
  import 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'`
    const response = NextResponse.next()
    response.headers.set('Content-Security-Policy', csp)
    response.headers.set('x-nonce', nonce)
    return response
  }
  ```

  Then pass the nonce to all `<Script>` components. See the Next.js CSP documentation for the complete pattern.

## External references

- cwe CWE-79
- cwe CWE-693
- owasp A03

Taxons: injection-and-input-trust

HTML version: https://auditbuffet.com/patterns/ab-002418
