Skip to content

Content Security Policy directives explained

A Content-Security-Policy header is one of the highest-leverage security headers a site can ship, and one of the most commonly skipped - because a naive policy breaks the third-party scripts most sites depend on. Here is what the directives actually do.

What CSP actually is and what it protects against

Content-Security-Policy is an HTTP response header that tells the browser an allowlist of where scripts, styles, images, frames and other resources are permitted to load from. It is enforced entirely by the browser, not your server, which is what makes it powerful against a specific class of attack: if an attacker manages to inject a script tag into your page (via a stored XSS vulnerability, a compromised third-party widget, or an unsanitised input reflected back into HTML), a correctly configured CSP stops that injected script from executing at all, even though the injection itself succeeded. It also stops your site being loaded inside someone else’s frame for clickjacking, via frame-ancestors.

script-src: the directive that matters most, and the one that gets gutted

script-src controls which scripts are allowed to run. The value unsafe-inline permits any inline <script> block or inline event handler to execute - which is exactly what XSS injection relies on, so allowing it removes most of CSP's protective value against injection while still technically having a policy. unsafe-eval similarly permits eval() and similar dynamic code execution. The safer pattern is nonces (a random token generated per page load, added to both the header and the specific script tags you trust) or hashes (a SHA hash of the exact script content you allow), combined with strict-dynamic, which lets a script you have explicitly trusted load further scripts it needs without you having to allowlist every downstream domain by name.

frame-ancestors: the modern anti-clickjacking directive

frame-ancestors controls which sites are allowed to embed yours in an iframe, and is the direct successor to the older X-Frame-Options header. Where X-Frame-Options only supports a blunt DENY, SAMEORIGIN, or a single ALLOW-FROM origin (and ALLOW-FROM is inconsistently supported across browsers), frame-ancestors accepts a proper list of allowed origins and is respected by all current browsers. Setting frame-ancestors to self (or none if the site should never be framed at all) closes off clickjacking attacks where your login or payment page is invisibly overlaid inside an attacker's page.

upgrade-insecure-requests and default-src as the floor

upgrade-insecure-requests tells the browser to automatically rewrite any http:// resource request on the page to https://, which is a cheap, effective fix for mixed-content warnings caused by old hardcoded http links in a codebase, without having to hunt down every instance manually. default-src sets the fallback allowlist for any resource type that does not have its own explicit directive - it is the floor the rest of the policy builds on, and a policy with a strong default-src but no script-src override typically leaves script-src just as permissive as default-src, which is a common configuration mistake.

Rolling it out safely without breaking your own site

Ship Content-Security-Policy-Report-Only alongside (or instead of) the enforcing header first. It sends the exact same evaluation but only reports violations - via the report-to directive to an endpoint you control - rather than blocking anything, which is how you find out what your policy would have broken before it actually breaks it. Run in report-only for at least a couple of weeks of real traffic, review the violation reports, tighten the allowlist to match what is genuinely needed, then switch to the enforcing header. Skipping this step is the single biggest reason CSP rollouts get abandoned after breaking checkout or login in production.

The honest cost, and why most sites ship none

Third-party scripts are the real obstacle: analytics tags, chat widgets, payment iframes, ad tech and tag managers each need their own domains allowlisted, and a tag manager that itself loads arbitrary further scripts (like Google Tag Manager) is fundamentally hard to constrain tightly without breaking whatever marketing or analytics tooling depends on it. This is exactly why most small-business sites ship no CSP at all - it looks like it will be a fight with the marketing stack, so nobody starts. A report-only rollout that only tightens what is provably safe to tighten gets most of the protection without that fight. AuditHQ’s security suite checks for CSP presence, weak script-src configuration, missing frame-ancestors and missing upgrade-insecure-requests as part of its header checks, and explains what each gap actually exposes.

Frequently asked questions

Is CSP worth it for a small business site?

Yes, at least at the frame-ancestors and upgrade-insecure-requests level, which are low-risk to add and close real gaps (clickjacking and mixed content) with almost no chance of breaking anything. A full script-src lockdown is more work because of third-party scripts, but even a moderate policy meaningfully reduces what an XSS injection can do if one is ever found.

What is the difference between frame-ancestors and X-Frame-Options?

They do the same job - controlling who can frame your page - but frame-ancestors is the modern CSP directive with consistent multi-origin support across all current browsers, while X-Frame-Options is older, more limited (effectively DENY or SAMEORIGIN in practice), and is being phased out in favour of CSP. Shipping both together is fine for backward compatibility, but frame-ancestors is the one that actually matters going forward.

Why does my CSP break Google Tag Manager?

GTM works by injecting further scripts dynamically at runtime, which is precisely the behaviour a strict script-src is designed to block. The usual fix is allowlisting GTM's own domain plus using strict-dynamic so a script GTM itself loads is trusted by extension, rather than trying to individually allowlist every tag GTM might ever fire.

Does CSP replace the need for input sanitisation?

No. CSP is a mitigating control that limits the damage if an injection succeeds - it is not a substitute for properly sanitising and encoding user input in the first place. Treat it as a second layer of defence, not the only one.