Tags: web-dev concept

Content Security Policy

Date: 2026-08-16


A header listing what the page is allowed to load and execute. It’s the main defence that turns a cross-site scripting bug from an account takeover into nothing — and it’s also the thing that breaks every tag manager on first deployment.


What it is

Content Security Policy (CSP) is an HTTP response header declaring which sources of scripts, styles, images, frames and connections the browser may honour. Anything not on the list is blocked.

Where The Same-Origin Policy governs what your page may read from elsewhere, CSP governs what it may load and run at all.

Content-Security-Policy:
  default-src 'self';
  script-src 'self' https://www.googletagmanager.com;
  style-src 'self' 'unsafe-inline';
  img-src 'self' https: data:;
  connect-src 'self' https://api.example.com;
  frame-ancestors 'none';

The directives that matter

DirectiveControls
default-srcFallback for anything not otherwise specified
script-srcJavaScript. The one that provides the security
style-srcStylesheets and inline styles
img-srcImages
connect-srcfetch, XHR, WebSocket, sendBeacon
frame-srcWhat may be embedded in your page
frame-ancestorsWho may embed you — the modern replacement for X-Frame-Options
form-actionWhere forms may submit. Stops an injected form exfiltrating data
base-uriPrevents an injected <base> redirecting every relative URL

frame-ancestors 'none' and base-uri 'self' are cheap, high-value, and routinely forgotten.

Why it works

An XSS vulnerability lets an attacker inject a script. CSP means the injected script has nowhere to run from — it isn’t on an allowed origin, and inline execution is blocked.

without CSP    injected <script> runs → reads cookies, storage, form data
with CSP       injected <script> blocked → the bug is still there, and inert

That’s the whole value proposition: defence in depth. It doesn’t fix the vulnerability; it removes the payoff.

unsafe-inline undoes it

The catch. Blocking inline scripts is what stops injected ones, so:

script-src 'self' 'unsafe-inline'      ← provides essentially no XSS protection

Any policy with 'unsafe-inline' in script-src is decorative. Since most sites have inline scripts — a tag manager snippet, a JSON blob, an analytics initialiser — this is where CSP projects usually stall.

The two ways out:

Nonces. A random value generated per response, put on both the header and each permitted inline script. An injected script can’t guess it.

Content-Security-Policy: script-src 'nonce-r4nd0m123'
 
<script nonce="r4nd0m123">dataLayer.push({...});</script>

The nonce must be unique per response and unguessable. A static nonce is worse than none, because it looks like protection.

Hashes. The SHA hash of the exact script contents. No server-side generation needed, but the hash changes whenever the script does, which makes it unworkable for anything dynamic.

The tag manager problem

This is where CSP meets commercial reality, and it’s worth being clear-eyed about.

A tag manager’s entire purpose is letting people add arbitrary third-party scripts without a deploy. CSP’s entire purpose is preventing arbitrary scripts from running. They are in direct opposition.

The options, none free:

  • Allowlist every vendor domain. Workable, but the list grows with every tag, and each entry is a trusted origin that could serve anything. 'strict-dynamic' — where a trusted script may load further scripts — is the usual compromise and it substantially widens the hole
  • Move tags server-side. One first-party endpoint, vendor calls made from your server. Fixes CSP, performance and ad blocking at once, and costs real engineering — Server-Side Tag Management
  • Accept a weaker policy on marketing pages and a strict one on checkout and account pages, where the data is. A defensible, common split

Deploying it

Never enforce first. Use report-only:

Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report

This blocks nothing and reports what would have been blocked. Run it for a fortnight, read the reports, then enforce.

Expect the reports to be noisy — browser extensions inject scripts into everyone’s pages and will generate violations you can’t act on. Filtering those out is most of the work of reading a CSP report stream.

Practical

  • Start report-only, always
  • frame-ancestors 'none' unless you’re deliberately embeddable — clickjacking protection for one line
  • base-uri 'self' — cheap, and closes a real injection route
  • Include connect-src or you’ll block your own analytics beacons and see it as a tracking bug
  • Audit the allowlist periodically. Every entry is a third party that can execute in your users’ sessions, and they accumulate — Third-Party Scripts, Supply Chain Risk