Content-Security-Policy (CSP) is an HTTP header that protects your site against XSS attacks and other threats. However, an overly restrictive CSP can block Google services like GTM, Analytics, or Maps. This guide shows how to configure CSP so all necessary services work.
TL;DR — ready-to-copy CSP for a typical site
If you just need a CSP header that works with GTM, GA4, Google Maps, Fonts, reCAPTCHA and YouTube, start with this and adjust:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-RANDOM' 'strict-dynamic'
https://www.googletagmanager.com
https://www.google-analytics.com
https://maps.googleapis.com
https://www.google.com/recaptcha/
https://www.gstatic.com/recaptcha/;
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
font-src 'self' https://fonts.gstatic.com;
img-src 'self' data: blob:
https://*.google-analytics.com
https://*.googletagmanager.com
https://*.gstatic.com
https://*.googleapis.com
https://*.ggpht.com
https://i.ytimg.com;
connect-src 'self'
https://*.google-analytics.com
https://*.analytics.google.com
https://*.googletagmanager.com
https://stats.g.doubleclick.net
https://maps.googleapis.com;
frame-src
https://www.googletagmanager.com
https://www.google.com
https://www.youtube.com
https://www.youtube-nocookie.com;
worker-src 'self' blob:;
object-src 'none';
base-uri 'self';
form-action 'self';
Replace RANDOM with a per-request nonce generated server-side. Start in Content-Security-Policy-Report-Only mode to see what your real site actually loads — only then switch to enforcement.
What is Content-Security-Policy?
CSP defines allowed sources for different types of resources (scripts, styles, images, fonts). The browser blocks everything that is not explicitly allowed.
Basic Syntax
Content-Security-Policy: directive source1 source2; directive2 source3;
Main Directives
| Directive | Controls |
|---|---|
default-src | Default source for all types |
script-src | JavaScript scripts |
style-src | CSS styles |
img-src | Images |
font-src | Fonts |
connect-src | XHR, fetch, WebSocket |
frame-src | Iframes |
object-src | Plugins (Flash, Java) |
Source Values
| Value | Meaning |
|---|---|
'self' | Same domain |
'none' | Block everything |
'unsafe-inline' | Allow inline (dangerous!) |
'unsafe-eval' | Allow eval() (dangerous!) |
'nonce-xyz' | Only scripts with this nonce |
'strict-dynamic' | Trust scripts loaded by trusted ones |
https: | All HTTPS sources |
*.example.com | Subdomain |
CSP for Google Tag Manager
GTM requires many domains and functionalities. Configuration depends on mode (client-side vs server-side).
GTM Client-Side - Minimal Configuration
Content-Security-Policy:
script-src 'self' 'unsafe-inline' 'unsafe-eval'
https://www.googletagmanager.com
https://tagmanager.google.com;
img-src 'self' data:
https://www.googletagmanager.com
https://www.google-analytics.com
https://ssl.gstatic.com
https://www.gstatic.com;
connect-src 'self'
https://www.google-analytics.com
https://analytics.google.com
https://stats.g.doubleclick.net
https://region1.google-analytics.com;
frame-src
https://www.googletagmanager.com;
Problem: GTM Requires unsafe-inline and unsafe-eval
GTM dynamically injects scripts, which requires 'unsafe-inline'. Custom HTML tags may use eval(). This significantly weakens CSP.
Solution 1: Nonce for GTM
<!-- Generate nonce server-side -->
<script nonce="abc123">
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;
j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl;
j.nonce='abc123';
f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-XXXXX');
</script>
Content-Security-Policy:
script-src 'self' 'nonce-abc123' 'strict-dynamic'
https://www.googletagmanager.com;
Solution 2: Server-Side GTM
Server-side GTM significantly simplifies CSP:
Content-Security-Policy:
script-src 'self' 'nonce-abc123'
https://gtm.yourdomain.com;
connect-src 'self'
https://gtm.yourdomain.com;
CSP for Google Analytics 4
GA4 with gtag.js
Content-Security-Policy:
script-src 'self'
https://www.googletagmanager.com;
img-src 'self'
https://www.google-analytics.com
https://www.googletagmanager.com;
connect-src 'self'
https://www.google-analytics.com
https://analytics.google.com
https://region1.google-analytics.com
https://region2.google-analytics.com
https://region3.google-analytics.com;
GA4 with GTM
If you load GA4 through GTM, you need a combination of the above. See also dataLayer best practices — a well-designed dataLayer lets you reduce the number of Custom HTML tags, which directly simplifies your CSP policy.
CSP for Google Maps
Maps JavaScript API
Content-Security-Policy:
script-src 'self' 'unsafe-inline'
https://maps.googleapis.com
https://maps.gstatic.com;
img-src 'self' data: blob:
https://maps.googleapis.com
https://maps.gstatic.com
https://*.ggpht.com
https://*.google.com
https://*.googleapis.com;
style-src 'self' 'unsafe-inline'
https://fonts.googleapis.com;
font-src 'self'
https://fonts.gstatic.com;
connect-src 'self'
https://maps.googleapis.com
https://places.googleapis.com;
frame-src
https://www.google.com;
worker-src blob:;
Maps Embed API (iframe)
Content-Security-Policy:
frame-src
https://www.google.com
https://maps.google.com;
CSP for Google Fonts
Content-Security-Policy:
style-src 'self'
https://fonts.googleapis.com;
font-src 'self'
https://fonts.gstatic.com;
Alternative: Self-hosting Fonts
Hosting fonts locally eliminates external dependencies:
Content-Security-Policy:
font-src 'self';
style-src 'self';
CSP for Google reCAPTCHA
reCAPTCHA v2/v3
Content-Security-Policy:
script-src 'self'
https://www.google.com/recaptcha/
https://www.gstatic.com/recaptcha/;
frame-src
https://www.google.com/recaptcha/
https://recaptcha.google.com;
style-src 'self' 'unsafe-inline';
CSP for YouTube Embeds
Content-Security-Policy:
frame-src
https://www.youtube.com
https://www.youtube-nocookie.com;
img-src 'self'
https://i.ytimg.com
https://img.youtube.com;
Gotcha 1: youtube-nocookie.com alone isn’t enough
Many sites use youtube-nocookie.com for privacy reasons — and whitelist only that domain in frame-src. That breaks the embed in several scenarios:
- Related videos overlay at the end of a clip redirects to
youtube.com. - Channel links and logo in the player bar open
youtube.com. - Some player chrome assets are still served from
youtube.comregardless of which domain you embed.
Fix: always whitelist both https://www.youtube-nocookie.com and https://www.youtube.com in frame-src, even if your <iframe src> points only to the no-cookie version.
Gotcha 2: thumbnail domains for lite embeds
If you use a “facade” pattern (click-to-play thumbnail instead of a full iframe) — popular for performance — the thumbnail image loads from a different domain than the player:
https://i.ytimg.com— default thumbnail host (vi/VIDEO_ID/maxresdefault.jpg).https://img.youtube.com— legacy alias, still used by some libraries.
Both need to be in img-src, otherwise the preview silently fails and users see a blank placeholder.
Gotcha 3: Cross-Origin-Opener-Policy breaking YouTube
If you harden your site with Cross-Origin-Opener-Policy: same-origin, the YouTube iframe loses access to popups it opens (sign-in, sharing dialog, full-screen). The player appears to work, but certain interactions fail silently with no CSP error in the console. Fix: switch to Cross-Origin-Opener-Policy: same-origin-allow-popups.
For a full breakdown of COOP, COEP, CORP, crossOriginIsolated, and how to unlock SharedArrayBuffer without breaking third-party integrations, see the dedicated guide: COOP, COEP, CORP — cross-origin isolation explained.
Production-ready example
Here is the exact snippet we use on uper.pl, combining GTM + GA4 + YouTube no-cookie embeds in a single header:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'unsafe-inline' 'unsafe-eval'
https://www.googletagmanager.com
https://tagmanager.google.com;
style-src 'self' 'unsafe-inline'
https://cdn.jsdelivr.net;
font-src 'self'
https://cdn.jsdelivr.net;
img-src 'self' data:
https://www.googletagmanager.com
https://www.google-analytics.com
https://ssl.gstatic.com
https://www.gstatic.com
https://i.ytimg.com;
connect-src 'self'
https://www.google-analytics.com
https://analytics.google.com
https://stats.g.doubleclick.net
https://*.google-analytics.com
https://*.analytics.google.com
https://*.googletagmanager.com;
frame-src
https://www.googletagmanager.com
https://www.youtube-nocookie.com
https://www.youtube.com;
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'self';
Paired with Cross-Origin-Opener-Policy: same-origin-allow-popups and X-Frame-Options: SAMEORIGIN, this combination passes Mozilla Observatory with an A+ and still lets YouTube embeds work end-to-end.
CSP for Google Ads
Google Ads Conversion Tracking
Content-Security-Policy:
script-src 'self' 'unsafe-inline'
https://www.googleadservices.com
https://www.googletagmanager.com
https://googleads.g.doubleclick.net;
img-src 'self'
https://www.google.com
https://googleads.g.doubleclick.net
https://www.googleadservices.com;
connect-src 'self'
https://www.google.com
https://pagead2.googlesyndication.com;
frame-src
https://bid.g.doubleclick.net
https://tpc.googlesyndication.com;
Google AdSense
Content-Security-Policy:
script-src 'self' 'unsafe-inline' 'unsafe-eval'
https://pagead2.googlesyndication.com
https://adservice.google.com
https://www.googletagservices.com
https://partner.googleadservices.com;
img-src 'self' data:
https://pagead2.googlesyndication.com
https://tpc.googlesyndication.com
https://*.google.com;
frame-src
https://googleads.g.doubleclick.net
https://tpc.googlesyndication.com
https://www.google.com;
style-src 'self' 'unsafe-inline';
CSP for Google Consent Mode v2
Consent Mode v2 is mandatory for EU traffic since March 2024. It uses the same domains as GTM + GA4, but introduces two wrinkles you need to account for:
- Cookiebot, Iubenda, OneTrust and other CMPs each load from their own domain — whitelist whichever one you use.
- Consent signals are sent before consent is granted (in
deniedstate), soconnect-srcto Google must be open from the first pageview, not after opt-in.
Content-Security-Policy:
script-src 'self' 'nonce-abc123'
https://www.googletagmanager.com
https://consent.cookiebot.com
https://consentcdn.cookiebot.com;
connect-src 'self'
https://consent.cookiebot.com
https://consentcdn.cookiebot.com
https://www.google-analytics.com
https://region1.google-analytics.com;
frame-src
https://consent.cookiebot.com
https://consentcdn.cookiebot.com;
See the Consent Mode v2 implementation guide and how to verify it’s working.
CSP for Google Identity Services (GIS) / One Tap
Google Identity Services replaced the old gapi.auth2 library. Sign-in with Google, One Tap, and the FedCM API all use these domains:
Content-Security-Policy:
script-src 'self'
https://accounts.google.com/gsi/client;
connect-src 'self'
https://accounts.google.com/gsi/
https://accounts.google.com/.well-known/;
frame-src
https://accounts.google.com/gsi/
https://accounts.google.com/o/oauth2/;
style-src 'self' 'unsafe-inline'
https://accounts.google.com/gsi/style;
Common gotcha: FedCM requires
frame-ancestorson Google’s side, not yours. If One Tap silently fails to render, checkaccounts.google.comin Network tab — a 403 usually means your site is missing from the Google Cloud OAuth client’s authorized origins, not a CSP issue.
CSP for Firebase (Auth, Firestore, Hosting)
Firebase spans several Google properties. Each product needs its own set of entries:
Content-Security-Policy:
script-src 'self'
https://www.gstatic.com/firebasejs/
https://apis.google.com
https://www.googleapis.com;
connect-src 'self'
https://*.firebaseio.com
https://*.firebaseapp.com
https://firestore.googleapis.com
https://identitytoolkit.googleapis.com
https://securetoken.googleapis.com
wss://*.firebaseio.com;
frame-src
https://*.firebaseapp.com;
Firebase Auth specifically uses signInWithRedirect which opens __/auth/handler on your Firebase Hosting subdomain — make sure to allow it in frame-src.
Complete Configuration for a Typical Site
Site with GTM, GA4, Maps, and Fonts
Content-Security-Policy:
default-src 'self';
script-src 'self' 'unsafe-inline' 'unsafe-eval'
https://www.googletagmanager.com
https://tagmanager.google.com
https://www.google-analytics.com
https://maps.googleapis.com
https://maps.gstatic.com;
style-src 'self' 'unsafe-inline'
https://fonts.googleapis.com
https://tagmanager.google.com;
img-src 'self' data: blob:
https://www.googletagmanager.com
https://www.google-analytics.com
https://ssl.gstatic.com
https://www.gstatic.com
https://maps.googleapis.com
https://maps.gstatic.com
https://*.ggpht.com
https://*.google.com;
font-src 'self'
https://fonts.gstatic.com;
connect-src 'self'
https://www.google-analytics.com
https://analytics.google.com
https://region1.google-analytics.com
https://stats.g.doubleclick.net
https://maps.googleapis.com;
frame-src
https://www.googletagmanager.com
https://www.google.com
https://www.youtube.com;
object-src 'none';
base-uri 'self';
form-action 'self';
Implementing CSP
Apache (.htaccess)
Header set Content-Security-Policy "default-src 'self'; script-src 'self' https://www.googletagmanager.com; ..."
Nginx
add_header Content-Security-Policy "default-src 'self'; script-src 'self' https://www.googletagmanager.com; ..." always;
Node.js (Express)
const helmet = require('helmet');
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "https://www.googletagmanager.com"],
styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
imgSrc: ["'self'", "data:", "https://www.google-analytics.com"],
fontSrc: ["'self'", "https://fonts.gstatic.com"],
connectSrc: ["'self'", "https://www.google-analytics.com"],
frameSrc: ["https://www.googletagmanager.com"]
}
}));
Netlify (netlify.toml)
[[headers]]
for = "/*"
[headers.values]
Content-Security-Policy = "default-src 'self'; script-src 'self' https://www.googletagmanager.com; ..."
Vercel (vercel.json)
{
"headers": [
{
"source": "/(.*)",
"headers": [
{
"key": "Content-Security-Policy",
"value": "default-src 'self'; script-src 'self' https://www.googletagmanager.com; ..."
}
]
}
]
}
Testing CSP
1. Report-Only Mode
Start with reporting mode, which doesn’t block but only logs:
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report;
2. Browser DevTools
Console will show CSP errors:
Refused to load the script 'https://example.com/script.js' because it violates the Content Security Policy directive: "script-src 'self'".
3. CSP Evaluator
https://csp-evaluator.withgoogle.com/ - Google’s tool for CSP analysis.
4. Observatory by Mozilla
https://observatory.mozilla.org/ - comprehensive security audit.
CSP Violation Reporting
Reporting Endpoint
Content-Security-Policy: default-src 'self'; report-uri /csp-report; report-to csp-endpoint;
Receiving Reports (Node.js)
app.post('/csp-report', express.json({ type: 'application/csp-report' }), (req, res) => {
console.log('CSP Violation:', req.body['csp-report']);
res.status(204).end();
});
Best Practices
1. Start Restrictively
Content-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self'; font-src 'self';
Add sources only when needed.
2. Avoid unsafe-inline and unsafe-eval
If possible, use nonce or hash instead of 'unsafe-inline':
<script nonce="random123">
// Your code
</script>
Content-Security-Policy: script-src 'nonce-random123';
3. Use strict-dynamic
'strict-dynamic' allows trusted scripts to load subsequent ones:
Content-Security-Policy: script-src 'nonce-abc123' 'strict-dynamic';
4. Consider Server-Side GTM
Server-side tagging drastically simplifies CSP and increases security.
5. Update Regularly
Google may change domains. Monitor CSP reports and update the policy.
Troubleshooting — common CSP errors and fixes
Below are the exact console messages you’ll see in Chrome / Firefox DevTools, and what to add to your policy to fix each one.
Refused to load the script 'https://www.googletagmanager.com/gtm.js'
Cause: script-src doesn’t include GTM. Fix: add https://www.googletagmanager.com to script-src.
Refused to apply inline style because it violates the following CSP directive: "style-src 'self'"
Cause: GTM, Google Maps, and reCAPTCHA all inject inline styles. Fix: add 'unsafe-inline' to style-src, or use a nonce if you control the injected markup. Hashes rarely work here because styles are generated dynamically.
Refused to execute inline script because it violates the following CSP directive
Cause: your GTM snippet or dataLayer push is inline without a nonce. Fix options:
- Add
'unsafe-inline'toscript-src(weakens CSP). - Add
nonce-xyzto every inline<script>tag AND toscript-src. Paired with'strict-dynamic', nonce lets GTM load downstream tags without listing every domain.
Refused to connect to 'https://region1.google-analytics.com/g/collect'
Cause: GA4 sends hits to region-specific endpoints (region1, region2, …). Fix: use a wildcard: https://*.google-analytics.com in connect-src.
Refused to load the image 'https://stats.g.doubleclick.net/...'
Cause: GA4 uses DoubleClick for cross-site measurement. Fix: add https://stats.g.doubleclick.net to both img-src and connect-src.
Refused to create a worker from 'blob:https://...'
Cause: Google Maps uses Web Workers created from blob URLs. Fix: add worker-src 'self' blob: (and blob: to script-src if you’re on an older CSP Level 2 without worker-src).
Refused to frame 'https://www.google.com/maps/embed'
Cause: Maps Embed uses frame-src, not img-src. Fix: add https://www.google.com to frame-src.
reCAPTCHA shows “Cannot contact reCAPTCHA”
Cause: usually connect-src is blocking the challenge endpoint. Fix: add https://www.google.com/recaptcha/ to both script-src and frame-src, and https://www.gstatic.com to script-src.
Reports flood to report-uri after deploy
Cause: browser extensions (password managers, ad blockers) inject scripts and trigger CSP violations you can’t do anything about. Fix: filter reports where blocked-uri starts with chrome-extension://, moz-extension://, or safari-web-extension:// before alerting.
How to roll out CSP safely
Deploying a strict CSP on an existing site will break something — guaranteed. Follow this sequence:
- Audit current load — open DevTools → Network → filter by domain. Collect every third-party host actually used. Our guide to checking cookies and trackers on a page is useful here so you don’t miss domains that only load after consent is granted.
- Deploy in Report-Only mode for at least 7 days. Production traffic will reveal edge cases dev/staging misses (legacy pages, email templates opened in-browser, A/B test variants).
- Route reports to a real endpoint — use Report URI or your own logger. Triage violations by frequency.
- Tighten directive by directive. Enforce
img-srcfirst (low risk), thenstyle-src,connect-src, and finallyscript-src(highest breakage risk). - Switch to enforce mode with the same policy. Keep the Report-Only header in parallel when testing a stricter next iteration.
- Revisit quarterly — Google adds domains (e.g.
analytics.google.com/g/collect, regionalregion1/2/3.google-analytics.com). Your policy needs to follow.
Check your CSP in real time with UPER SEO Auditor
If you want an ongoing sanity check on your Content-Security-Policy — without copy-pasting headers into online validators — install our Chrome extension UPER SEO Auditor. The Security tab:
- Parses your CSP into individual directives and shows every source side-by-side, so you can spot missing domains at a glance.
- Captures live CSP violations as you browse — every blocked resource is grouped by violated directive, with a
[Google]flag on blocks affecting Google services (GTM, GA4, Maps, Fonts, reCAPTCHA). - Warns if CSP is in Report-Only mode so you don’t deploy a weakened policy to production by mistake.
- Checks 10 security headers beyond CSP (HSTS, X-Frame-Options, Referrer-Policy, Permissions-Policy, COOP, COEP, CORP) with a weighted 0-10 score.
It’s free and runs entirely in your browser — no data leaves the page.
Summary
CSP for Google Services requires balancing security and functionality:
- GTM requires compromises —
'unsafe-inline'is often necessary unless you adopt nonce +'strict-dynamic' - Server-side GTM significantly simplifies CSP and removes most third-party script domains
- Nonce + strict-dynamic is the current best practice for script-src
- Consent Mode v2 needs
connect-srcopen to Google before consent is granted - Test in Report-Only for at least a week before enforcing
- Monitor violations continuously — Google domains drift over time
A well-configured CSP protects users against XSS without blocking site functionality.
Frequently Asked Questions about CSP for Google Services
Why does Google Tag Manager require unsafe-inline?
GTM injects inline scripts for its container and Custom HTML tags. Without unsafe-inline (or a matching nonce), the browser blocks those injections and GTM stops firing tags. The modern fix is to add a per-request nonce to the GTM snippet and use strict-dynamic, which lets GTM propagate trust to tags it loads without listing every domain.
Is it safe to use unsafe-eval with GTM?
unsafe-eval is only needed if your GTM container uses Custom HTML or Custom JavaScript tags that call eval() or new Function(). Most GA4 and Ads tags do not. Audit your container in Preview mode, remove legacy Custom HTML where possible, and drop unsafe-eval from your policy — it materially weakens XSS protection.
How do I fix "Refused to load googletagmanager.com" errors?
Add https://www.googletagmanager.com to script-src. If you also load GA4 via gtag.js through GTM, add https://www.google-analytics.com to img-src and connect-src, and https://*.google-analytics.com if you see regional endpoint errors.
Does Google Consent Mode v2 need special CSP changes?
Consent Mode v2 uses the same Google domains as GTM and GA4, so if those work, Consent Mode works. The key change is that connect-src must be open to Google analytics endpoints from the first pageview — Consent Mode pings Google in "denied" state before the user accepts cookies. Additionally, whitelist your CMP provider (Cookiebot, Iubenda, OneTrust, etc.).
Can server-side GTM eliminate the need for unsafe-inline?
Yes, and that is one of its biggest advantages. With server-side tagging, only a small first-party loader runs on your site (often just one script from your own subdomain), and the heavy tag logic runs on your server container. Your script-src can be reduced to self plus your tagging subdomain, with a nonce — no unsafe-inline, no third-party Google domains in the browser.
How do I test CSP without breaking my production site?
Use the Content-Security-Policy-Report-Only header. The browser processes your policy and sends violation reports to a report-uri endpoint, but never blocks anything. Run it in Report-Only for at least 7 days against real traffic, review reports, tighten the policy, then switch to the enforcing Content-Security-Policy header.
What is the difference between nonce and hash in CSP?
A nonce is a random value generated per request and attached to each trusted inline script tag; the browser only executes scripts whose nonce matches the one in the CSP header. A hash is a SHA-256 fingerprint of the script contents — useful when the script never changes, but brittle because any edit breaks it. Use nonces for dynamic content (GTM, React SSR) and hashes for static inline snippets.
Why are region1.google-analytics.com requests blocked?
GA4 routes collection hits to regional endpoints (region1, region2, region3.google-analytics.com) depending on user location. If you only whitelisted www.google-analytics.com, regional traffic is blocked. Use a wildcard: https://*.google-analytics.com in connect-src.
Sources
-
MDN - Content-Security-Policy https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
-
Google - CSP for Google Tag Manager https://developers.google.com/tag-platform/tag-manager/csp
-
Google - CSP Evaluator https://csp-evaluator.withgoogle.com/
-
web.dev - Content Security Policy https://web.dev/articles/csp
-
Google Maps Platform - CSP https://developers.google.com/maps/documentation/javascript/content-security-policy



