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.
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.
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.
ID: security-headers-ii.csp-quality.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:
// 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.