CSP Nonces and Hashes: Building a Strict Frontend Policy
Why strict CSP matters How to choose between CSP nonces and CSP hashes How to implement nonce-based CSP in the browser How to use hash-based CSP to tame static assets and builds How to monitor, report, and migrate to a s
- Why strict CSP matters
- How to choose between CSP nonces and CSP hashes
- How to implement nonce-based CSP in the browser
- How to use hash-based CSP to tame static assets and builds
- How to monitor, report, and migrate to a strict policy
- Practical application: checklist and code recipes
A strict Content Security Policy built around cryptographic nonces or hashes can make script injection impractical at the browser edge β but the wrong policy or a half-baked rollout will either break functionality or push teams to weaken protections. The goal isn't a policy that "blocks everything"; it's a policy that blocks the bad stuff while remaining predictable and automatable.
The site is full of small failures: analytics stops firing after a CSP rollout, A/B tests vanish, vendors complain their widgets were blocked, and someone reinstates unsafe-inline because "we had to ship." Those symptoms come from policies that aren't strict, are overly permissive, or were rolled out without an inventory and testing window β and that is why most CSP rollouts stagnate or regress into a false sense of security. CSP can protect you from script injection, but it only works when designed to match how your app actually loads and runs code.
Why strict CSP matters
A strict Content Security Policy (one that uses nonces or hashes instead of long allowlists) changes the attack model: the browser becomes the final gatekeeper that refuses to execute scripts unless they present a valid cryptographic token. That reduces the practical impact of reflected and stored XSS and raises the bar for exploitation.
Important: CSP is defense in depth. It reduces risk and attack surface but does not replace input validation, output encoding, or secure server-side logic. Use CSP to mitigate exploits, not as a substitute for fixing vulnerabilities.
Why a strict approach beats host-based allowlists
- Allowlist policies grow brittle and large (they often require enumerating dozens of domains to integrate common vendors).
- Strict CSPs based on
nonce-orsha256-β¦don't rely on hostnames, so attackers can't bypass them by injecting a script tag pointing at an allowed host. - Use tools like CSP Evaluator and Lighthouse to verify policies and avoid subtle bypasses.
Quick comparison
| Characteristic | Allowlist (host-based) | Strict (nonces/hashes) |
|---|---|---|
| Resistance to injected inline script | Low | High |
| Operational complexity | High (maintain hosts) | Medium (inject nonces or compute hashes) |
| Works well with dynamic scripts | Can be OK | Nonce-based: best. Hash-based: not ideal for large dynamic blobs. |
| Third-party support | Needs explicit hosts |
strict-dynamic + nonce makes third-party easier to support. |
How to choose between CSP nonces and CSP hashes
Start here: choose the mechanism that maps cleanly to how your UI is built.
-
Nonce-based CSP (nonce-based CSP)
- Best when pages are rendered server-side or you can inject a per-response token into templates.
- Nonces are generated per HTTP response and added both to the
Content-Security-Policyheader and to thenonceattribute on<script>and<style>tags. This makes dynamic inline bootstraps and SSR flows straightforward. - Use
strict-dynamicto allow scripts that your trusted (nonced) bootstrap loads; thatβs very helpful for third-party loaders and many libraries. Be mindful of older browser fallbacks when you rely onstrict-dynamic.
-
Hash-based CSP (CSP hashes)
- Best for static inline scripts or build-time-known fragments. Generate a
sha256-(orsha384-/sha512-) for the exact contents and place it in thescript-srclist. Changes to the script change the hash β include this in your build pipeline. - Hashes are ideal when you host static HTML and still need one small inline bootstrap or when you want to avoid templating to inject nonces.
- Best for static inline scripts or build-time-known fragments. Generate a
Trade-offs at a glance
- Generate nonces per response to avoid replay or guessing; use a secure RNG (see Node example later).
- Recalculating hashes is operational work, but itβs stable for static files and enabling SRI workflows.
-
strict-dynamicpaired with nonces/hashes reduces allowlist sprawl but changes how legacy fallbacks behave; test older browsers if you must support them.
How to implement nonce-based CSP in the browser
The core pattern:
- Generate a cryptographically secure, unpredictable nonce for each HTTP response. Use a secure RNG and base64 or base64url encode the result.
- Add the nonce to the
Content-Security-Policyheader as'nonce-<value>'. Use the same nonce value in thenonceattribute of inline<script>/<style>elements that you trust. - Prefer
strict-dynamicin modern browsers to reduce host-based allowlists; provide a safe fallback if you must support older clients.
A minimal Node/Express pattern
// server.js (Express)
const express = require('express');
const crypto = require('crypto');
const app = express();
app.use((req, res, next) => {
// 16 bytes -> 24 base64 chars; you can choose a larger size
const nonce = crypto.randomBytes(16).toString('base64');
// Store for templates
res.locals.nonce = nonce;
// Example strict header (adjust directives to your needs)
res.setHeader(
'Content-Security-Policy',
`default-src 'none'; script-src 'nonce-${nonce}' 'strict-dynamic'; object-src 'none'; base-uri 'none'`
);
next();
});
// In your templating engine (EJS example)
// <script nonce="<%= nonce %>">window.__BOOTSTRAP__ = {...}</script>
// <script nonce="<%= nonce %>" src="/static/main.js" defer></script>
app.listen(3000);
Notes and gotchas
- Generate a unique nonce per response; don't reuse across users or over time. Use
crypto.randomBytes(Node) or a secure RNG on your platform. - Do not implement a dumb middleware that re-writes every script tag to add a nonce after the fact; templating is safer. If an attacker can inject HTML into the template stage, they will get the nonce alongside their payload. OWASP warns against naive noncing middleware.
- Avoid inline event handlers (e.g.,
onclick="...") β they are incompatible with strict policies unless you useunsafe-hashes, which weakens protection. PreferaddEventListener. - Keep the CSP header on the server (not in meta tags) for reporting and
Report-Onlyflexibility. Meta tags can't receivereport-onlyreports and have limitations.
Trusted Types and DOM sinks
- Use the
require-trusted-types-for 'script'andtrusted-typesdirectives to enforce that only sanitized, policy-created values reach DOM XSS sinks likeinnerHTML. This makes DOM-based XSS much easier to audit and reduce. Treat Trusted Types as the next step after you have nonces/hashes in place.
How to use hash-based CSP to tame static assets and builds
When you have static inline blocks (for example, a small inline bootstrap that sets up window.__BOOTSTRAP__), compute the base64 SHA hash and add it to script-src. This is perfect for CDNs, static hosting, or very small inlines that rarely change.
Generating a hash (examples)
- OpenSSL (shell):
# produce a base64-encoded SHA-256 digest of the exact script contents
echo -n 'console.log("bootstrap");' | openssl dgst -sha256 -binary | openssl base64 -A
# result: <base64-hash>
# CSP entry: script-src 'sha256-<base64-hash>'
- Node example (build step):
// compute-hash.js
const fs = require('fs');
const crypto = require('crypto');
const script = fs.readFileSync('./static/inline-bootstrap.js', 'utf8');
const hash = crypto.createHash('sha256').update(script, 'utf8').digest('base64');
console.log(`sha256-${hash}`);
Add to your CSP header or inject into HTML meta in build-time pipelines. For long-term maintainability:
- Integrate the hash generation into your build (Webpack, Rollup, or a small Node script).
- For external scripts prefer Subresource Integrity (SRI) plus
crossorigin="anonymous"; SRI protects supply-chain tampering, while CSP prevents execution of injected inline payloads. - Remember: any change (even whitespace) alters the hash. Use CI to regenerate hashes automatically and fail builds when mismatches occur.
Browser compatibility nuance
- CSP Level 3 expanded some hash semantics and added features like
strict-dynamic; older browsers may behave differently with certain hash-and-external-script combos. Test the set of browsers you must support and consider a fallback (e.g.,https:in the policy) for legacy clients.
How to monitor, report, and migrate to a strict policy
A staged rollout avoids breaking production users and gives you data to make the policy precise.
Reporting primitives
- Use
Content-Security-Policy-Report-Onlyto collect violation reports without blocking. Browsers send reports that you can consume and analyze. - Prefer the modern Reporting API: declare endpoints with the
Reporting-Endpointsheader and reference them withreport-toinside your CSP.report-uriremains present in the wild but is deprecated in favor ofreport-to/Reporting API.
Example headers (server-side):
Reporting-Endpoints: csp-endpoint="https://reports.example.com/csp"
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'nonce-<token>'; report-to csp-endpoint
Collect and triage
- Accept
application/reports+jsonon your report endpoint and store minimal metadata (URL, violated directive, blocked URI, user-agent, timestamp). Avoid logging user-supplied content verbatim to your logs. - Run two parallel stages: a broad report-only rollout to collect noise, then a tightened policy in enforcement mode for a subset of routes before full enforcement. Web.dev's guidance maps this process.
Use automated tools in your pipeline
- Run policies through CSP Evaluator to spot common bypass patterns before deploying.
- Use Lighthouse in CI to catch missing or weak CSPs on entry pages.
A conservative migration timeline (example)
- Inventory: scan your site for inline scripts, event handlers, and third-party scripts (1β2 weeks).
- Create a draft strict policy (nonce or hash) and deploy in
Report-Onlysite-wide (2β4 weeks of collection; longer for low-traffic services). - Triage: sort reports by frequency and impact; fix code to stop relying on blocked patterns (replace inline handlers, add nonces to legitimate bootstraps, add hashes for static inlines).
- Stage enforcement on a subset of traffic or routes. Monitor.
- Enforce globally once violations are rare or have known mitigations. Automate hash regeneration in CI for hashed policies.
Practical application: checklist and code recipes
Practical checklist (high-signal tasks)
- Inventory: export a list of pages with inline code, external scripts, and event handlers.
- Decide policy style: nonce-based for SSR/dynamic apps; hash-based for static sites.
- Implement nonce generator with a secure RNG and pass it to templates.
crypto.randomBytes(16).toString('base64')is a sensible default in Node. - Add
Content-Security-Policy-Report-OnlyandReporting-Endpointsto collect violations. - Triage and fix top violations; remove inline handlers and move to
addEventListener. - Convert
Report-OnlytoContent-Security-Policyand enforce. - Add
require-trusted-types-for 'script'and allow-listedtrusted-typespolicies when ready to lock down DOM sinks. - Add SRI for critical external scripts to protect supply chain risk.
- Automate policy checks in CI with CSP Evaluator and browser-based smoke tests (headless runs capturing console errors).
Report endpoint example (Express):
// small receiver for Reporting API / CSP reports
const express = require('express');
const app = express();
// browsers POST JSON with Content-Type: application/reports+json
app.post('/csp-report', express.json({ type: 'application/reports+json' }), (req, res) => {
// Persist to a datastore or analytics. Avoid echoing the full report into public logs.
console.log('CSP report received:', JSON.stringify(req.body, null, 2));
res.status(204).end();
});
Automated hash generation (build step snippet):
// build/hash-inline.js
const fs = require('fs');
const crypto = require('crypto');
function hashFile(path) {
const content = fs.readFileSync(path, 'utf8');
const hash = crypto.createHash('sha256').update(content, 'utf8').digest('base64');
return `sha256-${hash}`;
}
// example usage
console.log(hashFile('./static/inline-bootstrap.js'));
Policy example (final enforcement header):
Content-Security-Policy:
default-src 'none';
script-src 'nonce-<server-generated>' 'strict-dynamic';
object-src 'none';
base-uri 'none';
require-trusted-types-for 'script';
trusted-types myPolicy;
Key operational rules
- Verify your policy with CSP Evaluator before enforcement.
- Keep the report endpoint accessible only from browsers (rate-limit and validate).
- Don't fall back to
unsafe-inlineas a permanent fix. That defeats the purpose of strict CSP.
Strong finishing thought
A strict, well-instrumented CSP built from nonces and hashes turns the browser into an active defender without needlessly breaking functionality β but it requires planning: inventory, secure nonce generation, build-time automation for hashes, and a patient report-only rollout. Treat CSP as an operational feature that your CI and monitoring pipelines own; do the work once, automate it, and the policy becomes a stable, high-leverage protection for years to come.
Sources:
Content Security Policy (CSP) - MDN - Core CSP concepts, examples for nonce and hash-based strict policies and general guidance.
Mitigate cross-site scripting (XSS) with a strict Content Security Policy (web.dev) - Practical rollout steps, strict-dynamic guidance, and browser-fallback recommendations.
Content Security Policy - OWASP Cheat Sheet - Operational cautions, nonce warnings, and rollout advice.
Content-Security-Policy: script-src directive - MDN - nonce, strict-dynamic, unsafe-hashes, and event-handler behavior.
Reporting API - MDN - Reporting-Endpoints, report-to, report format (application/reports+json) and collection guidance.
Content-Security-Policy: report-uri directive - MDN (Deprecated) - Notes deprecation and suggests migrating toward report-to / Reporting API.
Node.js Crypto: crypto.randomBytes() - Use secure RNG for nonces (crypto.randomBytes).
Trusted Types API - MDN - Using trusted-types and require-trusted-types-for to lock down DOM sinks.
Subresource Integrity (SRI) - MDN - Generating integrity hashes and using SRI for external resources; examples for openssl command usage.
google/csp-evaluator (GitHub) - Tooling to validate CSP strength and detect common bypasses.
Ensure CSP is effective against XSS attacks (Lighthouse docs) - Integration points for audits and CI checks.
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes β full credit and traffic to the original publisher.