The day a security review lands on your desk usually plays out the same way. Someone runs Mozilla Observatory or a Lighthouse audit against your production app. The report flags a missing Content-Security-Policy header. You open MDN, scan thirty-something directives, write a “looks reasonable” policy, and ship it. Two days later your error tracker fills up with broken stripe.js, broken Google Fonts, and a Sentry inbox that scrolls forever. You roll back the policy. Six months later the same thing happens again.
CSP is one of the most powerful browser-side defenses against cross-site scripting and clickjacking, and one of the easiest things to get wrong. The directive set is large, the syntax is unforgiving, three different header names ship subtly different behavior, and the relationship between hashes, nonces, and 'unsafe-inline' is not obvious until you have read the spec twice. ZeroTool’s CSP Header Generator gives you a workbench: pick a preset, click the keyword chips you need, paste an inline script to compute its hash, and copy the result as an HTTP header, an HTML <meta> tag, an Express middleware snippet, or an Nginx directive. This guide walks through how to use it, the design decisions baked into the strict preset, and the pitfalls the tool’s validation panel exists to flag.
Why a CSP at all
A Content Security Policy is an HTTP response header. The browser parses it once when a page loads and uses it to allow or block every resource that page tries to fetch — scripts, stylesheets, images, fonts, frames, connections, prefetches. Without a CSP a page can load any URL it wants. With a strict CSP a page can only load what you have explicitly trusted, which means even when an attacker manages to inject a <script> tag into your HTML, the browser refuses to execute it. The same mechanism protects against framing attacks via frame-ancestors, base-tag hijacking via base-uri, and form exfiltration via form-action.
CSP does not replace input sanitization, output encoding, or template auto-escaping. It is the last line of defense, and it is the line that catches XSS payloads that slipped through everything else. Modern guidance from the W3C CSP3 spec, Google’s CSP Evaluator, and OWASP’s strict CSP guide all converge on the same skeleton: nonces or hashes for inline scripts, 'strict-dynamic' so descendant scripts inherit trust, 'self' for first-party resources, and 'none' for object-src because legacy plugin content is the single largest XSS bypass surface.
Five common workflows
| Scenario | Starting preset | Notable directive setup |
|---|---|---|
| Greenfield SPA, no third-party CDNs | Strict | script-src 'self' 'strict-dynamic' + per-request nonce |
| Marketing site with Google Fonts and Analytics | Moderate | font-src 'self' fonts.gstatic.com, script-src with hashes for inline GA bootstrap |
| Legacy app with inline event handlers everywhere | Basic in Report-Only | Watch the violation reports, then add 'unsafe-hashes' for the specific handlers you cannot rewrite |
| Internal admin tool, want to lock down framing | Strict | frame-ancestors 'none' so it cannot be embedded anywhere |
| API gateway response previewer | Strict + report-only | default-src 'self', connect-src 'self', then enforce after one week of clean reports |
Each of these starts from a different preset because the trade-off between security and breakage is different. A marketing site that breaks because Google Analytics stops loading is worse than one with a slightly looser policy. An admin panel that lets attackers iframe it into a phishing kit is unacceptable. The tool’s preset selector encodes those four positions on the curve so you can pick a starting point and adjust from there.
How the workbench is laid out
When you open the page the toolbar at the top picks the preset and the mode. The Strict preset matches the OWASP “strict CSP” recommendation: 'self' plus 'strict-dynamic' on script-src, 'none' on object-src, 'self' on base-uri and form-action. Moderate adds 'unsafe-inline' to style-src because legacy CSS-in-JS libraries still emit inline styles, and adds https: to img-src because most product sites pull images from a few CDNs. Basic gives you only default-src 'self' so you can build the policy directive by directive. Empty starts blank for the cases where you already know exactly what you want.
The Mode toggle switches between Content-Security-Policy (Enforce) and Content-Security-Policy-Report-Only (Report-Only). Report-Only sends violation reports to your report-uri or report-to endpoint without blocking anything. This is the mode you deploy first.
Below the toolbar, every directive is a card. Each card has three rows of source pickers: keywords ('self', 'none', 'strict-dynamic', 'unsafe-inline', 'unsafe-eval'), schemes (https:, data:, blob:, mediastream:), and a free-form input for hosts, paths, hashes, and nonces. The + nonce button generates a fresh base64 token using crypto.getRandomValues. Clicking a chip toggles the source on; clicking the × on a tag removes it.
The hash calculator at the bottom of the form takes the contents of an inline <script> or <style> block — exactly the bytes between the opening and closing tags — and runs them through SHA-256, SHA-384, or SHA-512 via the Web Crypto API. The output is the format CSP expects: 'sha256-<base64>'. One click adds it to script-src or style-src, depending on which one you picked from the dropdown.
The output panel renders four tabs:
- HTTP header — what your origin server sends. This is the canonical form.
- HTML
<meta>— a fallback for static hosts where you cannot set headers. The tool warns you thatframe-ancestors,report-uri,report-to, andsandboxare silently ignored when CSP is delivered via<meta>. - Express (helmet) — drop-in code for Node servers using helmet. Note
useDefaults: falseso your policy is not silently merged with helmet’s built-in defaults. - Nginx —
add_header ... always;directive. Thealwaysflag matters because without it Nginx skips the header on error responses, leaving the 500 page unprotected.
The validation list under the output flags suspicious combinations live: 'none' mixed with other sources, 'unsafe-inline' neutered by hashes, frame-ancestors placed in a <meta> policy, missing default-src, http: schemes that allow insecure fetches.
Hashes vs. nonces, and why you usually want both
Inline <script>...</script> blocks are the single most common reason CSP rollouts get rolled back. The naive fix is to add 'unsafe-inline' to script-src. That works, but it disables the part of CSP that catches XSS — the entire point of the policy.
The correct fix is to allow specific inline scripts. Two mechanisms do this:
A hash is the SHA-256 (or 384 or 512) digest of the exact bytes inside the <script> tag, base64-encoded. If the script content changes by one character, the hash no longer matches and the browser blocks it. Hashes are deterministic and cache-friendly: you can precompute them at build time and ship them in a static header. Hashes are the right choice for analytics snippets, critical CSS, and any inline content that ships with the page and rarely changes.
A nonce is a random base64 token your server generates per request. The server emits a header containing 'nonce-XYZ' and stamps the same XYZ on every <script nonce="XYZ"> tag it renders. Browsers execute scripts whose nonce attribute matches; everything else is blocked. Nonces are the right choice for server-rendered HTML where the inline content varies (CSRF tokens, user IDs, page-specific bootstraps).
The hash calculator on the page is for the static case. For the dynamic case, click + nonce to generate a sample nonce and replace it server-side at request time.
The two combine well with 'strict-dynamic'. Once a script is trusted (via hash or nonce), 'strict-dynamic' says any script that script loads is also trusted, transitively. This means you do not have to enumerate every CDN your trusted script might pull from. It also stops a class of bypasses where an attacker abuses a legitimately allow-listed CDN to host their own payload, because 'strict-dynamic' overrides host-based allow-lists for descendant scripts.
A subtle point: when a hash or nonce is present in script-src, modern browsers ignore 'unsafe-inline'. This is the spec’s “strict mode”: if you are sophisticated enough to use hashes or nonces, the browser treats 'unsafe-inline' as a backwards-compatibility hint for older browsers and otherwise discards it. The validation panel in the tool surfaces this as an info-level note so you do not panic when you see both keywords listed.
The five pitfalls the validator watches for
'none' must be alone
The CSP3 spec is clear: when 'none' appears in a source list, every other source is dropped. Writing script-src 'none' https://cdn.example.com is a bug — the browser ignores the CDN. The validator flags this as a hard warning.
frame-ancestors is HTTP-only
The HTML <meta http-equiv="Content-Security-Policy"> tag exists, but four directives are deliberately ignored when CSP arrives that way: frame-ancestors, sandbox, report-uri, and report-to. The reason is that these directives need to take effect before the document is fully parsed, and the meta tag is parsed mid-document. If you need framing protection on a static host like GitHub Pages, you have to send a real header — usually via a CDN or a worker.
Missing default-src removes your fallback
Several fetch directives (script-src, style-src, img-src, etc.) fall back to default-src when not specified. New directives added in future CSP revisions also fall back to default-src for compatibility. If you skip default-src entirely, your policy is brittle: the day a browser ships a new directive, your site silently allows it. Even a single default-src 'self' line is better than none.
http: is not the same as “no policy”
It is tempting to write img-src https: http: so your CDN works in dev and prod. The http: scheme allows insecure fetches over plaintext, which means a coffee-shop attacker with a Wi-Fi pineapple can MITM the response and replace your image with anything. Use https: only, and use upgrade-insecure-requests if you need to deal with legacy http:// URLs in your own HTML. The validator highlights http: sources as info-level warnings.
Custom hosts cannot contain semicolons
CSP uses semicolons to separate directives. A semicolon inside a host or path terminates the policy and turns every directive after it into garbage. The tool refuses to add sources that contain semicolons.
Deploying the generated header
Once you have a policy you like, the four output tabs cover the common deployment surfaces. Here is the rough mental model for each.
# Nginx: usually inside the server { } block
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'strict-dynamic'; object-src 'none'; base-uri 'self'" always;
# Apache: in .htaccess or httpd.conf, "always" is implicit
Header always set Content-Security-Policy "default-src 'self'; script-src 'self' 'strict-dynamic'; object-src 'none'; base-uri 'self'"
// Express + helmet
const helmet = require('helmet');
app.use(helmet.contentSecurityPolicy({
useDefaults: false,
directives: {
"default-src": ["'self'"],
"script-src": ["'self'", "'strict-dynamic'", (req, res) => `'nonce-${res.locals.cspNonce}'`],
"object-src": ["'none'"],
"base-uri": ["'self'"],
}
}));
# Cloudflare Pages _headers file
/*
Content-Security-Policy: default-src 'self'; script-src 'self' 'strict-dynamic'; object-src 'none'; base-uri 'self'
For Vercel, paste the same string into vercel.json’s headers array. For Netlify, add it to netlify.toml. For static export to S3 + CloudFront, configure CloudFront response-headers policies. The point is that the policy text is the same everywhere; only the wrapping config syntax changes.
A staged rollout plan that actually works
Almost every successful CSP deployment follows the same shape:
- Pick the Strict preset in the tool, deploy as
Content-Security-Policy-Report-Onlywith areport-uri(orreport-togroup) endpoint. Do not enforce yet. - Watch reports for one to two weeks. You will see violations from genuine code paths (an inline
onclick, a forgotten CDN), browser extensions injecting scripts, and prerender bots. - Triage: violations from your own code get fixed (move inline handlers to addEventListener, add a hash for the analytics snippet you cannot move). Violations from extensions get filtered server-side. Violations from prerender bots usually mean tightening one directive.
- Tighten: rebuild the policy in the tool with the lessons from step 3. Maybe
script-srcneeds a hash for one specific inline boot snippet. Maybeconnect-srcneeds your error tracker’s domain. - Switch to enforce by changing the header name from
Content-Security-Policy-Report-OnlytoContent-Security-Policy. The tool’s Mode toggle does this for you in one click. - Keep collecting reports even after enforcement. New SDKs, new third-party scripts, new pages will trip the policy. The reporting endpoint is your early-warning system.
The two failure modes that derail this plan are skipping step 1 (going straight to Enforce on day one) and treating the report endpoint as write-only (collecting reports but never reading them). The tool encourages step 1 by defaulting to Report-Only mode when you pick the Strict preset for the first time. The reporting endpoint is on you.
How this differs from existing CSP tools
Report URI’s CSP Wizard and csper.io both build policies by watching real traffic from your site, which is great if you have already deployed CSP and want to refine it. They are not great if you are starting from zero and want a sane preset on day one. The OWASP CSP Generator is a JSON form with no preview and no validation. The various Chrome extensions that show your current site’s CSP are useful for inspection but not for authoring.
ZeroTool’s tool is smaller in scope and cleaner in execution. There is no account, no traffic capture, no telemetry — your policy never leaves the browser tab. The validation panel surfaces the five pitfalls that actually break deployments instead of cataloguing every theoretical violation. The hash calculator runs in the browser via SubtleCrypto so you can paste production code without worrying about it being logged. The four output formats cover the deployment surfaces you actually use, in the syntax those surfaces actually accept.
Further reading
- W3C Content Security Policy Level 3 — the spec.
- OWASP Content Security Policy Cheat Sheet — practical defense in depth.
- Google’s Strict CSP guide — the case for nonces +
'strict-dynamic'. - MDN: Content-Security-Policy — directive reference.
- ZeroTool Meta Tag Generator — companion tool for the SEO/social half of your
<head>block. - ZeroTool Hash Generator — when you need raw SHA-256 outside the CSP context.
- ZeroTool .htaccess Generator — for the Apache deployment surface.
- ZeroTool Robots.txt Generator — the other half of crawler control.
CSP is not a one-and-done configuration. It is a living policy that changes as your dependencies, third-party scripts, and threat model change. Keep the generator open in a tab during the next security review.