Content Security Policy Generator: The Strict CSP Approach, Not the Outdated Allowlist

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

Include https: fallback (recommended unless you only need to support current browsers)
‘unsafe-eval’ — only check this if you’ve confirmed eval() usage can’t be refactored out, since it meaningfully weakens this policy

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

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

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top