CSP Nonce vs Hash vs Strict-Dynamic: Pick Right or Don't Bother
Cybersecurity
Most Content Security Policies in production are either trivially bypassable or cause silent breakage. Choosing nonce, hash, or strict-dynamic correctly is what separates real defence from compliance theatre.
By Arjun Raghavan, Security & Systems Lead, BIPI · September 4, 2024 · 8 min read
I reviewed a CSP for a healthcare SaaS last quarter. The policy looked impressive: 28 directives, allowlists for analytics vendors, a report-uri pointing at their SIEM. It was also bypassable in three different ways. unsafe-inline was still in script-src 'just for the legacy admin panel'. The CDN they allowlisted hosted JSONP endpoints. And one of the analytics vendors had a known XSS that returned attacker-controlled JS.
That CSP didn't reduce XSS risk. It made the security team feel productive while spending three engineering quarters arguing about it.
What the three options actually do
Nonce-based CSP issues a fresh random value per response and embeds it in every legitimate inline script tag. The browser executes scripts whose nonce attribute matches and blocks the rest. Pros: works for inline scripts, easy to integrate when you control the template. Cons: requires server-side rendering or middleware to inject the nonce, breaks aggressive HTML caching.
Hash-based CSP whitelists specific script bodies by SHA-256 hash. Pros: works with static HTML, no server-side state needed. Cons: any change to the inline script breaks CSP, doesn't help with externally-loaded scripts, gets unwieldy past a handful of inline blocks.
Strict-dynamic propagates trust from a nonce or hash to scripts those scripts load. Pros: solves the third-party transitive loading problem (Google Tag Manager, A/B test scripts, etc). Cons: requires CSP3 support, ignores host-based allowlists when present (which surprises people).
The mistake we see most often
Teams ship a CSP with both 'nonce-XXX' and 'https://cdn.example.com' in script-src, thinking they have defence-in-depth. They don't. The host allowlist is the weak link: if any URL on that CDN can return attacker-controlled JS (JSONP, file upload, open redirect to a JS file), the nonce is useless. The right move is to drop host allowlists entirely and rely on strict-dynamic.
Practical CSP for an SPA
For a React or Vue app behind a Next.js or similar framework, the policy that actually works looks roughly like this. script-src uses a nonce plus strict-dynamic, no host allowlists. style-src allows nonce or hash for inline styles, plus 'self' for stylesheet files. img-src is permissive (it's hard to attack via images). connect-src is locked to your API hosts. frame-ancestors is 'none' unless you have a real reason to be embedded.
The nonce gets injected by middleware on every request. Bundlers like webpack and Vite have plugins that mark loader scripts with the nonce. Third-party scripts (Segment, Sentry, Stripe) load from your bundled entry point, so strict-dynamic propagates trust to them automatically.
Report-only is a tool, not a destination
Every CSP rollout should start in Content-Security-Policy-Report-Only mode. Ship the report-uri to a collector you can actually query (a Cloudflare Worker into ClickHouse, or a managed service like report-uri.com). Run for two to four weeks, fix the violations, then flip to enforce.
The teams that get stuck never flip. They sit in report-only forever because the noise floor never drops to zero. The trick is triaging violations by source: legitimate inline scripts get nonces, third-party scripts get fixed or dropped, and browser extension noise gets filtered out at the collector.
When CSP isn't worth the effort
If your app has zero inline scripts, no third-party JS, and a small static asset surface, a basic host-based CSP is fine. The complex nonce-plus-strict-dynamic setup pays off when you have analytics, A/B testing, payment widgets, and real-time features that pull dynamic JS.
The honest assessment: CSP is one of the highest-effort, highest-value web security controls available. Done correctly it shuts down most XSS classes. Done lazily it generates a checkbox for the audit and nothing else.
Read more field notes, explore our services, or get in touch at info@bipi.in. Privacy Policy · Terms.