← QACSP

What's the General CSP Pattern That Lets Tag Managers and Vendor Scripts Run Without unsafe-inline?

Answer in brief

The durable pattern is a nonce-based Content Security Policy: your server generates a random, single-use nonce value on every page request, includes it in the CSP header (script-src 'nonce-<value>'), and stamps that same nonce onto every legitimate <script> tag - including the tag manager's own container script and any inline Custom HTML tags it injects. This lets the browser allow scripts carrying a matching, freshly-generated nonce while blocking any injected script that doesn't, which is exactly the protection unsafe-inline throws away entirely.

Why this happens

Content Security Policy exists to stop injected or malicious scripts from executing, and unsafe-inline defeats that purpose by allowing any inline script to run - which is why security-conscious teams disable it, and why tag managers immediately become a problem: a tag manager's whole design is injecting scripts into the page dynamically, often via inline script content inside Custom HTML tags, which is precisely what a strict CSP is built to block.

The naive fix - adding unsafe-inline back - reintroduces the exact vulnerability CSP exists to close, so it isn't a real fix, only a rollback of the security posture.

Nonces solve this because they're single-request, unpredictable values: an attacker attempting a script-injection attack has no way to know or guess the nonce for the current page load, so their injected script fails the CSP check even though the tag manager's legitimately nonce-stamped scripts pass. The nonce must be freshly generated per request - never reused, never hardcoded - or it stops providing any real protection.

Fix it

  1. 1

    Generate a per-request nonce on the server

    This requires server-side rendering or a server that can inject a value into the response - a fully static site with no request-time server logic cannot generate a genuinely per-request nonce (a hash-based CSP source is the fallback there, covered below). Generate a cryptographically random value per request and make it available to your templating layer.

  2. 2

    Include the nonce in both the CSP header and every script tag

    Set the CSP header as script-src 'self' 'nonce-<value>' (plus any specific vendor domains you allow), and add nonce="<value>" as an attribute to your GTM/GA4 script tags - using the exact same value generated in step one. A mismatch between the header’s nonce and the tag’s nonce attribute fails just like having no nonce at all.

  3. 3

    Propagate the nonce into GTM's own injected Custom HTML tags

    GTM's Custom HTML tag type can read and apply the page's active nonce automatically in recent versions - confirm your setup is on a version that supports nonce handling for inline scripts. Older or hand-rolled Custom HTML tags may need the nonce value passed in manually via a variable.

  4. 4

    For static sites without per-request server logic, use hash-based CSP as the fallback

    If nonces aren't feasible, script-src can allow a specific inline script by its exact SHA-256 hash instead. This is more brittle - any change to the script's exact content invalidates the hash - so it's better suited to a small number of stable, rarely-changing inline scripts than to a tag manager's frequently-changing tag configuration.

  5. 5

    Explicitly allowlist third-party domains vendor tags actually call out to

    A nonce covers script execution on your own page, but vendor tags typically also load their own subsequent scripts and make network requests to other domains - those need explicit entries in script-src and connect-src or they'll be blocked by CSP even with a correct nonce on the container script itself.

House Differentiator

How to verify it worked

Open DevTools’ Console tab immediately after page load, with your new CSP in place.

Passing: No "Refused to execute inline script" or CSP-violation errors appear, and GTM’s tags still fire correctly in GTM Preview mode.

Still broken: If you see CSP violation errors naming a specific script, that script is either missing its nonce attribute, using a stale/mismatched nonce, or calling out to a domain not yet allowlisted.

View page source and confirm the nonce value in the CSP response header exactly matches the nonce attribute on your script tags, for a fresh page load.

Passing: The values match, and reloading the page produces a new, different nonce value on both the header and the tags simultaneously.

Still broken: If the header's nonce changes on reload but the script tag's nonce attribute stays the same (or vice versa), your templating layer isn't correctly propagating one generated value to both places - check for stale caching at the CDN or server layer.

Related

If this is happening on a client-critical property, we'll verify it directly.

A data audit checks this exact failure mode against your actual implementation, not a generic checklist.