Search “CSP generator” and nearly every result produces the same thing: a form with checkboxes for script-src, style-src, and a text box to list allowed domains. This is the CSP everyone learned to build a few years ago, and it’s also the version security researchers and MDN’s own documentation now describe as fundamentally weak, easy to bypass with an actual XSS vulnerability, and hard to maintain as your list of third-party domains grows. The recommended approach today looks meaningfully different, and almost nothing free online actually generates it correctly.
This tool builds a strict CSP using nonces and the strict-dynamic keyword, the pattern MDN and current security guidance both point to as the one that actually resists XSS rather than merely looking like it does. It also explains, directive by directive, why this approach works and the old allowlist style doesn’t, since copying a header without understanding what it’s actually doing is exactly how people end up with a CSP that either breaks their site or provides much less protection than it appears to.
Generate a Strict CSP
What CSP Actually Does
Content Security Policy is an HTTP response header that tells the browser which sources of scripts, styles, images, and other resources are allowed to load and execute on a page. Its primary real-world purpose is mitigating cross-site scripting, XSS, by making it much harder for an attacker’s injected script to actually run, even if they’ve successfully gotten malicious markup into your page through an unescaped user input field or a vulnerable third-party widget.
Without CSP, a successful XSS injection generally just runs, since the browser has no independent way to distinguish a script your application intentionally included from one an attacker snuck in. CSP gives the browser that distinction, refusing to execute anything that doesn’t match the policy, regardless of how it ended up in the page’s HTML.
Why Domain Allowlists Are Considered Outdated
The traditional approach, listing specific trusted domains in script-src and allowing scripts from any of them, has a well-documented, serious flaw: research into real-world CSP deployments found that the large majority of domain-based allowlists can be circumvented by an attacker who already has an XSS bug to exploit, because it’s extremely common for one of the allowed domains to itself host a script, a JSONP endpoint, an old library version, an analytics snippet, that can be abused to execute arbitrary attacker-controlled code. Once that happens, the allowlist isn’t protecting anything, since the attacker’s payload is technically loading from an “allowed” domain.
Allowlists are also a genuine maintenance burden that tends to grow rather than shrink. Every new third-party script, analytics tool, payment widget, chat plugin, font provider, adds another domain that needs to be added and kept current, and a stale allowlist either breaks a new integration or, more commonly, gets loosened over time until it’s permissive enough to stop meaningfully restricting anything.
Nonces, Explained Properly
A nonce is a unique, cryptographically random value generated fresh on the server for every single HTTP response, included both in the CSP header itself and as an attribute on any inline script tag you actually intend to allow. The browser only executes an inline script if its nonce attribute matches the value declared in that response’s CSP header. Since the value changes on every request, an attacker injecting a script tag into your page has no way to know or predict the correct nonce, so their injected script simply doesn’t run, regardless of how it got onto the page.
The nonce has to genuinely change every request
A nonce that’s hardcoded or reused across multiple page loads defeats the entire mechanism, since an attacker who can see the page’s HTML once can see the reused nonce and simply include it in their own injected script. This means a strict CSP genuinely requires server-side rendering capable of generating a fresh random value and injecting it into both the response header and the HTML on every request, which is a real architectural requirement, not just a configuration setting.
strict-dynamic, and Why Modern Frameworks Need It
Modern JavaScript frameworks, and common tools like Google Tag Manager, frequently load additional scripts dynamically at runtime, creating new elements via JavaScript rather than declaring them all directly in the initial HTML. A basic nonce-based policy alone would block all of these dynamically created scripts, since they weren’t part of the original HTML and don’t carry the nonce attribute themselves.
The strict-dynamic keyword solves this by extending trust: once a script has been validated by a matching nonce or hash, any additional script that trusted script goes on to load is automatically trusted too, creating a chain of trust rooted in that first, verified script. This is specifically what makes a strict CSP practical for real applications using frameworks that hydrate or inject scripts dynamically, rather than forcing every single script on the page to be individually nonce’d by hand. Google Tag Manager specifically, often cited as the reason teams give up on CSP entirely, works cleanly under this pattern: nonce the initial GTM bootstrap script, and strict-dynamic propagates trust to whatever GTM subsequently injects.
One deliberate side effect worth understanding: when strict-dynamic is present, browsers that support it will ignore any domain-based allowlist in the same script-src directive entirely, relying purely on the nonce or hash chain instead. This is intentional, for backward compatibility with older policies, and it’s why a strict CSP and a domain allowlist aren’t really meant to be combined as the primary mechanism, one supersedes the other in supporting browsers.
Hash-Based CSP for Fully Static Content
If your site is fully static, generated once at build time with no server available to inject a fresh nonce into every response, hash-based CSP is the alternative. Instead of a nonce, you compute the SHA-256 hash of an inline script’s exact content and include that hash directly in the CSP header. The browser computes the same hash over the actual script content on the page and only executes it if the hashes match.
The trade-off is that every distinct inline script needs its own separately computed hash, and any change to that script’s content, even a single character, changes its hash and requires updating the CSP to match. This makes hash-based CSP considerably more fragile for actively developed applications with frequently changing inline scripts, but it’s a legitimate, secure option specifically for static site generators and cached content where server-side per-request nonce generation genuinely isn’t available.
object-src, base-uri, and frame-ancestors
object-src 'none' disables Flash, Java applets, and other plugin-based embeds entirely, which is recommended as a default in nearly every current strict CSP guide, since these legacy embed types have their own long history of security vulnerabilities largely unrelated to your own application code, and most modern sites have no legitimate use for them at all.
base-uri controls what the page’s tag is allowed to point to. Without restricting this, an attacker who manages to inject a base tag can redirect every relative URL on the page, including script and stylesheet references, to a domain they control, which is exactly the kind of attack this directive exists to close off. Setting it to 'none' is the strictest option and appropriate for most applications.
frame-ancestors controls who is allowed to embed your page inside an iframe, and it’s the current, CSP-based replacement for the older X-Frame-Options header, which is now considered legacy. Setting this to 'none' prevents any site, including your own on a different path, from framing the page at all, which is the standard, sensible default for pages that have no legitimate reason to be embedded elsewhere, and a meaningful defense specifically against clickjacking attacks.
Rolling Out a New CSP Without Breaking Your Site
Deploying a brand-new CSP directly in enforcing mode on an existing, already-live application is a reliable way to discover, in production, exactly how many things your policy accidentally breaks. The safer, standard approach is deploying the identical policy using the Content-Security-Policy-Report-Only header instead of the enforcing Content-Security-Policy header first. In report-only mode, the browser evaluates the policy and reports violations to a specified endpoint, but doesn’t actually block anything, giving you real visibility into what would break before it actually does.
A reasonable rollout timeline runs the report-only policy for at least several days to a week, long enough to capture traffic across your application’s different pages and user flows, reviewing the violation reports and fixing genuine issues, whether that means adding a nonce to a script you’d missed or refactoring a pattern the policy correctly identifies as risky. Only once violations have settled down to genuinely expected, reviewed exceptions should the policy switch over to the enforcing header.
Some CSP directives also interact with less obvious parts of a browser’s behavior worth knowing about before you’re surprised by them in testing. Service workers, for instance, are governed by the CSP attached to the response that served the worker script itself, not the CSP of the page that registers it, which means a restrictive page-level policy won’t automatically apply the same way to worker code, and the simplest practical approach for many applications is avoiding sending a CSP header on service worker script responses entirely rather than trying to craft one that fits both contexts.
Framework-Specific Implementation
Node.js / Express
Generating and applying a fresh nonce per request fits naturally as middleware, setting the header and making the nonce available to your templating layer for inclusion in script tags:
const crypto = require('crypto');
app.use((req, res, next) => {
const nonce = crypto.randomBytes(16).toString('base64');
res.locals.nonce = nonce;
res.setHeader(
'Content-Security-Policy',
\`script-src 'nonce-\${nonce}' 'strict-dynamic'; \` +
\`object-src 'none'; base-uri 'none'; frame-ancestors 'none';\`
);
next();
});
Every template rendering a tag needs the matching nonce attribute pulled from res.locals.nonce, which typically means a small update across your templating layer wherever inline scripts currently exist.
PHP / Laravel
The same pattern applies through middleware, generating the nonce once per request and sharing it with your views:
$nonce = base64_encode(random_bytes(16));
header("Content-Security-Policy: script-src 'nonce-$nonce' 'strict-dynamic'; object-src 'none'; base-uri 'none';");
// share $nonce with your Blade views, e.g. via View::share('cspNonce', $nonce)
In Blade templates, every inline script needs nonce="{{ $cspNonce }}" added directly on the tag for the browser to trust it under the policy.
Fully static sites
Without a server generating per-request nonces, hash-based CSP set at your hosting or CDN configuration level is the practical option, computing SHA-256 hashes for your build’s inline scripts at build time and injecting them into the deployed headers as part of your build process.
Real Scenarios
A student project with a few inline scripts
A basic nonce-based CSP is worth setting up even here, both as a genuine security improvement and as a demonstration of understanding a real, current security practice rather than skipping it entirely. Start in report-only mode, confirm nothing breaks, then enforce.
A single-page application using Google Tag Manager and analytics
The nonce plus strict-dynamic pattern specifically solves this case, nonce the GTM bootstrap script and let strict-dynamic propagate trust to whatever it subsequently injects, rather than trying to enumerate every analytics-related domain in an allowlist that will inevitably grow stale.
A fully static site with no server-side rendering
Hash-based CSP, computed as part of your static build process, is the right fit here, accepting the added maintenance of updating hashes when inline script content changes as the trade-off for not needing a server to generate per-request nonces.
Common Mistakes
Reusing the same nonce value across multiple requests, sometimes by hardcoding it directly into a template rather than generating it fresh server-side, defeats the entire security property nonces are meant to provide, since a predictable or static nonce is exactly as bypassable as having no nonce at all.
Adding 'unsafe-inline' back into a policy that also includes a nonce, often to quickly silence a violation during initial rollout, is a common, quiet way to neutralize a strict CSP without realizing it. Browsers that support nonces ignore 'unsafe-inline' when a nonce or hash is present in the same directive, so this specific combination doesn’t cause immediate breakage, but it does mean older browsers without nonce support will fall back to allowing all inline scripts, silently weakening protection specifically for those users without any obvious symptom during testing.
Skipping the report-only rollout phase entirely and deploying directly in enforcing mode is a mistake that tends to surface as a confusing wave of broken functionality across a live application, exactly the outcome the report-only phase exists to catch safely beforehand.
And treating CSP as a complete substitute for proper input sanitization and output encoding, rather than a defense-in-depth layer on top of them, misunderstands what CSP is actually for. It’s a mitigation that limits the damage an XSS vulnerability can do if one exists, not a replacement for actually preventing the injection in the first place through proper escaping and validation.
FAQ
Do I need a server to implement a strict CSP?
For nonce-based CSP, yes, since a fresh nonce needs to be generated and injected into both the header and the HTML on every request. Fully static sites without server-side rendering should use hash-based CSP instead, computed at build time.
Will strict-dynamic break my existing domain allowlist?
In browsers that support strict-dynamic, yes, a domain-based allowlist in the same script-src directive is ignored in favor of the nonce or hash chain. This is intentional, and it’s why a strict CSP generally replaces an allowlist approach rather than combining with it as the primary mechanism.
How long should I run CSP in report-only mode before enforcing it?
Long enough to capture traffic across your application’s different pages and user flows, typically at least several days to a week, reviewing and fixing genuine violations before switching to the enforcing header.
Does CSP protect against every type of XSS?
A well-implemented strict CSP significantly raises the bar against stored, reflected, and DOM-based XSS variants, but it’s a mitigation layer, not a complete substitute for proper input sanitization and output encoding in your application code.

