CSP uses nonce or hash-based script allowlisting
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.
// 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-srcdirective. 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 viagenerateCspNonce(). -
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.comwithout 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:
// 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:2021 · A03
Taxons
History
- 2026-04-18·v1.0.0·Initial import from security-headers-ii·automated