BIPI
BIPI

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

#csp#web-security#xss

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.

37%
Top 1k sites with CSP that's bypassable per Google's CSP Evaluator
94%
CSPs that include 'unsafe-inline' in script-src
<5%
Sites using strict-dynamic correctly

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.