KB Building Content Security Policy

Content Security Policy researched

Script-src is the whole game in CSP hardening. Build the right policy for your platform — domain allowlist for pragmatic baseline, nonce + strict-dynamic for real XSS protection.

Use this when

Hardening a client site against script injection after an Aikido or similar finding, launching a new HubSpot or Webflow site with third-party scripts, or remediating an unsafe CSP before going live.

Definition of done

The CSP is deployed in Report-Only mode with a live violation-reporting endpoint. A report feed shows zero (or expected, minor) violations on golden-path pages. The policy is then enforced, and you monitor for 48–72 hours before declaring success.

The three hardening approaches

CSP's protection hinges on script-src. No script-src (or a default-src fallback missing both) means scripts run unrestricted, even if other directives look good. upgrade-insecure-requests, for instance, doesn't constrain scripts at all.

Three ways to constrain script-src, ranked by protection:

Approach Syntax Strength Best for
Domain allowlist script-src 'self' *.hubspot.com cdn.example.com Weak. JSONP gadgets or script-injection on allowlisted hosts bypass it. Inline is blocked, which is the main win. Platform-hosted sites (HubSpot, Webflow) where you can't inject per-request nonces. Pragmatic, common baseline.
Nonce + strict-dynamic script-src 'nonce-RANDOM' 'strict-dynamic' Strong. OWASP/Google recommended. Only scripts carrying the per-request nonce run; trusted scripts may load further scripts without being allowlisted. Sites where you control HTML rendering or can inject at the edge (Cloudflare Workers).
Hashes script-src 'sha256-BASE64HASH' Strong for fixed inline scripts; brittle if content changes frequently. A known, fixed set of inline scripts that don't change per request.

Nonce + strict-dynamic mechanics

A nonce is a random value regenerated per request, placed both in the CSP header and on each trusted <script> tag. Server generates it fresh for every response; attacker sees only that one response's nonce and cannot reuse it. Deploying a static nonce is security theater — it offers zero protection.

The 'strict-dynamic' keyword extends trust from a nonced script to scripts it creates dynamically (via document.createElement('script')), so you don't have to allowlist every CDN a trusted loader pulls from. This simplifies the policy dramatically.

Browser support: 'strict-dynamic' in Chrome 52+, Edge 79+, Firefox 52+, Safari 15.4+ (verified 2026-05-28).

Graceful degradation — why 'unsafe-inline' alongside a nonce is safe

Modern policy stacks fallbacks for older browsers:

script-src 'nonce-abc123' 'strict-dynamic' 'unsafe-inline' https:

How browsers parse this:

So adding 'unsafe-inline' *alongside* a nonce is safe and recommended for back-compat — modern browsers ignore it. This is the opposite of the naive "unsafe-inline = bad" reading.

Cloudflare edge-nonce pattern

If your site sits behind Cloudflare, two paths to CSP:

Critical: an edge-set CSP overrides whatever the origin emits. Pick one owner of the header. If you set CSP at Cloudflare, don't also rely on HubSpot's native nonce feature — the edge header wins and has no nonce in it.

HubSpot-specific gotchas

HubSpot CMS has a native nonce feature — when enabled, HubSpot auto-generates a per-request nonce for HubSpot-hosted and HubSpot-injected scripts. But the feature has real limits:

HubSpot's nonce feature only helps if HubSpot emits the CSP header. If an edge (Cloudflare) sets/overrides the header, the nonce in HubSpot's HTML won't match the edge header unless the edge generates it. See the Cloudflare section above.

Reporting — where violations go

Modern CSP has two reporting directives:

Options: managed (report-uri.com, Sentry, CentralCSP) or self-hosted (csplogger, a small POST endpoint logging application/csp-report JSON). Without a live reports feed, a Report-Only phase tells you nothing. Confirm the endpoint is actually ingesting before counting the monitoring window.

Decision tree

Is the site platform-hosted (HubSpot or Webflow) with many third-party scripts? A domain-allowlist script-src without 'unsafe-inline' is the pragmatic baseline and clears scanner findings. Accept it as "good," not "ideal."

Does the client need real XSS hardening / strong security posture? Move to nonce + strict-dynamic. Choose the header owner per platform: let HubSpot own it (enable its native nonce, no edge CSP), or own it at Cloudflare (use a Worker for nonce injection). Budget for breakage in the HubSpot gotchas above.

Always run Report-Only first with a live reports endpoint, and keep style-src 'unsafe-inline' as an accepted lower-risk exception on themed CMS sites.

Gotchas

Static nonces are worthless. If the nonce is the same for every request, an attacker reads it from the HTML and injects a matching script. Per-request generation is mandatory.

upgrade-insecure-requests is not a script policy. It only rewrites HTTP sub-resource URLs to HTTPS. It says nothing about what scripts may execute. Don't confuse it with script-src.

CSP is a backup, not a substitute for encoding/sanitization. WAFs do not catch DOM-based XSS. Code-review custom JavaScript regardless; CSP catches only inline injection.

Avoid allowlist CSPs if you can. Domain allowlists are easily bypassed (JSONP gadgets, script-injection on allowlisted hosts) and bloat fast — Google Analytics alone can require ~187 allowlisted domains. Strict nonce/hash beats allowlist if your platform supports it.

Why & sources

CSP is one line of defense in a layered XSS strategy. Full cited research: Web security for client sites. This page synthesizes the latest guidance from OWASP CSP Cheat Sheet, web.dev Strict CSP, MDN script-src, and Cloudflare's CSP documentation (verified 2026-05-28). Cross-link the security baseline page for the broader XSS defense picture.