How to Fix Content Security Policy (CSP) Headers and Mixed Content Errors That Break Third-Party Scripts
You added a security header and analytics stopped reporting. Nothing errored on the server, nothing errored in the app. Here is how CSP breaks tracking, and the config that fixes it.

Content Security Policy is one of the highest-value security headers you can ship. It is also the one most likely to break your marketing stack, because it fails in the browser rather than on the server. Your logs stay clean, your monitoring stays green, and analytics quietly reports nothing.
The failure is specific and recognisable: everything works on the developer's machine with extensions disabled, and conversion tracking dies in production for the subset of browsers that enforce the policy strictly.
How to confirm CSP is the cause
Open the live page with the console visible. CSP violations are loud once you look:
Refused to load the script 'https://www.googletagmanager.com/gtm.js?id=GTM-XXXX' because it violates the following Content Security Policy directive: "script-src 'self'".
Read the current policy from the headers rather than guessing:
curl -sI https://example.com | grep -i 'content-security-policy'
Content-Security-Policy-Report-Only for a week before enforcing. The browser reports what would have been blocked without blocking anything, which turns a production incident into a log to read.The directives that actually matter
| Directive | Controls | Common omission |
|---|---|---|
default-src | Fallback for everything not listed | Set to 'self' then forgetting the rest |
script-src | JavaScript sources | GTM, GA, payment SDK, reCAPTCHA |
script-src-elem | <script> tags specifically | Overrides script-src where present — must list the same hosts |
connect-src | fetch, XHR, WebSocket, beacon | Analytics endpoints differ from script hosts |
img-src | Images, including tracking pixels | data: URIs and pixel domains |
style-src | Stylesheets | Inline styles need 'unsafe-inline' |
font-src | Web fonts | data: and the CDN serving them |
frame-src | iframes | Payment iframes, reCAPTCHA challenge |
Two traps account for most of the debugging time. First, `connect-src` is separate from `script-src` — allowing googletagmanager.com to load the script does not allow the beacon to google-analytics.com, so the tag loads, fires, and the hit never leaves. Second, `script-src-elem` overrides `script-src` for <script> elements when it is present, so a policy that sets both must list the hosts in both.
A working baseline
This covers GTM, GA4, a payment SDK, reCAPTCHA and a consent tool — the stack most Indian D2C and SaaS sites run. Trim it to what you actually use rather than pasting it wholesale.
add_header Content-Security-Policy "
default-src 'self';
script-src 'self' 'unsafe-inline' 'unsafe-eval'
https://www.googletagmanager.com
https://www.google-analytics.com
https://cdn-cookieyes.com
https://www.google.com https://www.gstatic.com;
script-src-elem 'self' 'unsafe-inline'
https://www.googletagmanager.com
https://www.google-analytics.com
https://cdn-cookieyes.com
https://www.google.com https://www.gstatic.com;
connect-src 'self'
https://www.google-analytics.com
https://analytics.google.com
https://region1.google-analytics.com
https://www.googletagmanager.com
https://api.yourbackend.com;
img-src 'self' data: https:;
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
font-src 'self' data: https://fonts.gstatic.com;
frame-src 'self' https://www.google.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
" always;The always flag matters: without it, Nginx omits the header on error responses, so your 404 and 500 pages ship unprotected.
Apache, same policy on one line:
<IfModule mod_headers.c> Header always set Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://www.google-analytics.com; connect-src 'self' https://www.google-analytics.com https://analytics.google.com; img-src 'self' data: https:; style-src 'self' 'unsafe-inline'; frame-ancestors 'none'; base-uri 'self'" </IfModule>
On Cloudflare, set it as a Transform Rule under Rules → Transform Rules → Modify Response Header, static value. Avoid setting it in two places — an origin header and a Cloudflare header both present results in the browser enforcing the intersection, which is almost never what you intended.
About unsafe-inline
The baseline above includes 'unsafe-inline' because GTM injects inline scripts and removing it breaks the container. That is a real weakening of the policy and worth being honest about rather than pretending otherwise.
The stronger approach is a per-request nonce: generate a random value each response, put it on your own inline scripts, and reference it in the policy. GTM supports a nonce-aware snippet. It is meaningfully more work — the nonce must be generated per request and never cached — so the pragmatic sequence is to ship the working policy first and tighten afterwards, rather than shipping nothing while you plan the perfect one.
<script nonce="{{REQUEST_NONCE}}">
window.dataLayer = window.dataLayer || [];
</script>
<!-- and in the header -->
<!-- script-src 'self' 'nonce-{{REQUEST_NONCE}}' https://www.googletagmanager.com; -->Mixed content: the other half
Mixed content is an HTTPS page loading an http:// resource. Browsers block active mixed content — scripts, iframes, XHR — outright, and flag passive mixed content like images. The visible symptom is a "Not secure" indicator on a page that has a valid certificate, which is particularly damaging on a checkout.
Find it in the built output rather than the source, since build tools and CMS fields both introduce it:
# Insecure references in the served HTML curl -s https://example.com | grep -oE '(src|href)="http://[^"]*' | sort -u # And in a WordPress database, the usual source # wp search-replace 'http://example.com' 'https://example.com' --dry-run
The usual sources are hardcoded URLs in CMS content, an old theme or plugin, email-signature images, and third-party embeds that default to HTTP. Fix them at the source. upgrade-insecure-requests in the CSP will rewrite them for you, but treat that as a safety net rather than the fix — it masks the problem and does nothing for a resource that has no HTTPS version.
Roll it out without an incident
- Ship
Content-Security-Policy-Report-Onlywith your intended policy and areport-uri. - Leave it a week. Real traffic surfaces sources you did not know about — a chat widget, a remarketing pixel someone added in GTM.
- Add the legitimate hosts. Investigate anything you do not recognise; an unexplained script host is what CSP is for.
- Switch to enforcing mode during low traffic.
- Watch analytics volume for 24 hours. A sudden drop means a beacon is being blocked — check
connect-srcfirst. - Re-test after any new marketing tool is added. A tag added in GTM can be blocked by a policy nobody thought to update.
That last point is where the two halves of this article meet. Marketing adds tags through GTM without touching the codebase; security tightens headers without touching GTM. Neither team sees the other's change, and conversion data disappears. A scheduled check that verifies both the security headers and the presence of your tags catches it in a day rather than a quarter.
The Security & Malware and Tracking modules report CSP directives, mixed content, SSL status and whether GTM, GA4 and your pixels are actually firing on the live page.
Scan my site