Google Analytics 4, loaded by the Google tag (gtag.js) or by Tag Manager, needs script-src https://www.googletagmanager.com, img-src https://www.googletagmanager.com https://*.google-analytics.com and connect-src https://www.googletagmanager.com https://*.google-analytics.com https://*.google.com. That is the list in Google’s tag platform CSP guide for Analytics without Ads features. The inline snippet that starts the tag needs a nonce too: Google recommends one over 'unsafe-inline'.
The policy, with a nonce
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-REPLACE_ME' https://www.googletagmanager.com; img-src 'self' https://www.googletagmanager.com https://*.google-analytics.com; connect-src 'self' https://www.googletagmanager.com https://*.google-analytics.com https://*.google.com; object-src 'none'; base-uri 'self'; frame-ancestors 'self'
Your server swaps REPLACE_ME for a fresh random value on every response and puts the same value on the snippet: <script nonce="…">. Google’s nonce-aware GTM snippet copies it onto gtm.js, and Tag Manager passes it on to the scripts it adds. Google’s table says script-src-elem; script-src covers it, because script-src-elem falls back to script-src.
The builder rates this medium: a nonce beside a host allowlist, without 'strict-dynamic', still trusts everything on www.googletagmanager.com, and that host serves any container, an attacker’s included. The other finding is low: a <meta> tag ignores frame-ancestors, so send the policy as a header.
The strict variant
script-src 'nonce-REPLACE_ME' 'strict-dynamic' https://www.googletagmanager.com; object-src 'none'; base-uri 'none'
Google’s guide doesn’t mention 'strict-dynamic'. Under CSP Level 3 it trusts whatever the nonced snippet loads, so gtm.js and the tags it adds run, while browsers that support it ignore the host. The host stays for browsers without 'strict-dynamic', such as Safari before 15.4. Tap the 'strict-dynamic' chip in the builder below and the medium finding goes. With gtag.js, put the nonce on both of its <script> tags: under 'strict-dynamic' a host no longer vouches for a script in the HTML.
Gotchas
- Regional hosts.
gtag.jssends hits to a collection subdomain, such asregion1.google-analytics.comandregion1.analytics.google.com. A*.wildcard matches any depth of subdomain, sohttps://*.google-analytics.comandhttps://*.google.comcover both. Google’s guide of January 2025 listedhttps://*.analytics.google.cominstead. - Ads features need more. Google suggests adding them up front:
img-srcaddshttps://*.google.com https://*.google.<TLD> https://*.g.doubleclick.net,connect-srcaddshttps://*.google.<TLD> https://*.g.doubleclick.net https://pagead2.googlesyndication.com, plusframe-src https://www.googletagmanager.com. CSP has no wildcard for the end of a host, so each Google country domain is listed on its own. - Custom JavaScript variables come out
undefinedunlessscript-srchas'unsafe-eval', which the builder rates high. Google recommends custom templates instead, which don’t need it. A template that injects a script or sends a pixel still needs the hosts it reaches: inimg-srcorconnect-src, and inscript-srcunless'strict-dynamic'is on. - Preview mode needs
https://tagmanager.google.cominscript-src,style-src https://www.googletagmanager.com https://tagmanager.google.com https://fonts.googleapis.com,https://ssl.gstatic.com https://www.gstatic.cominimg-src, andfont-src https://fonts.gstatic.com data:. - Roll it out in Report-Only. Send
Content-Security-Policy-Report-Onlywith areport-toendpoint first: nothing is blocked, and every violation is reported.
In Express
import crypto from 'node:crypto';
import helmet from 'helmet';
app.use((req, res, next) => {
res.locals.cspNonce = crypto.randomBytes(16).toString('base64'); // 128 bits, new for every response
next();
});
app.use(helmet({
contentSecurityPolicy: {
directives: {
scriptSrc: ["'self'", (req, res) => `'nonce-${res.locals.cspNonce}'`, "'strict-dynamic'", 'https://www.googletagmanager.com'],
imgSrc: ["'self'", 'https://www.googletagmanager.com', 'https://*.google-analytics.com'],
connectSrc: ["'self'", 'https://www.googletagmanager.com', 'https://*.google-analytics.com', 'https://*.google.com'],
},
},
}));