Content-Security-Policy on a landing page: allowing GTM, Meta Pixel and Stripe without blocking everything
Published on 1 September 2026 · 8 min read
A visitor who lands on a landing page from a Meta ad or a Google Ads campaign triggers, without knowing it, the execution of half a dozen scripts that no line of the site's own code wrote directly: the Google Tag Manager container, the Meta pixel, the Google Ads conversion tag, sometimes a customer-reviews widget, and at checkout time the js.stripe.com script. The browser runs each of them with exactly the same level of trust it gives code the team wrote itself. By default, there is no rule distinguishing "this script comes from Google and was added on purpose" from "this script comes from a compromised third-party provider nobody ever noticed." The Content-Security-Policy (CSP) HTTP header exists to set exactly that rule: an allowlist of what may execute, enforced by the browser itself, independent of whatever the page's HTML happens to contain.
Why a landing page with no user accounts is still exposed
The most common objection: a landing page has no member area and no sensitive data to protect, so CSP would seem reserved for authenticated applications. That reasoning misses the real attack surface. A landing page is still a web page that accepts input — a lead-capture form, sometimes a review or comment field — and that embeds dynamically displayed content, like a third-party testimonials widget or a Google reCAPTCHA tag. Each of these is a theoretical entry point for a script injection (XSS): if an attacker manages to push a malicious string through one of those channels, CSP is the last barrier still standing once server-side validation has already been bypassed. It's defense in depth, not protection against an unlikely scenario — it doesn't replace input validation, it catches what validation would have let through.
What a CSP directive actually does
A CSP policy is made of directives, each restricting one category of resource to a list of allowed sources. script-src controls which domains are allowed to serve executable JavaScript; style-src does the same for stylesheets; img-src, connect-src (network calls via fetch/XHR) and frame-src (content shown in an iframe, like a Stripe widget) cover the rest. Any resource that tries to load from a domain missing from its matching list is blocked by the browser — silently for the visitor, but logged in the developer console, and, if report-uri or report-to is configured, reported as a JSON payload sent to a chosen URL. That reporting channel is what makes it possible to discover, before an incident forces the issue, that a poorly coded widget is trying to load a resource from an unexpected domain.
Which domains to allow for a landing page's tracking + payment stack
The most common blocker on a first CSP rollout is how many subdomains each marketing tool actually uses — often more than its own install docs mention. The table below covers the typical stack of a landing page using Google Tag Manager, the Meta pixel, and Stripe for payment.
| Tool | Directive | Domains |
|---|---|---|
| Google Tag Manager | script-src | *.googletagmanager.com |
| GA4 (loaded via GTM) | connect-src | *.google-analytics.com, *.analytics.google.com |
| Meta Pixel | script-src, connect-src | connect.facebook.net, *.facebook.com |
| Google Ads (conversion) | script-src, connect-src | *.googleadservices.com, *.google.com |
| Stripe (Checkout) | script-src, frame-src, connect-src | js.stripe.com, checkout.stripe.com, api.stripe.com |
| Google Fonts (if not self-hosted) | style-src, font-src | fonts.googleapis.com, fonts.gstatic.com |
One detail that trips up nearly every first configuration: Google Tag Manager itself loads other scripts once it runs (GA4, a pixel added from its own interface), coming from domains different from the initial GTM container. Allowing only googletagmanager.com and forgetting google-analytics.com produces a container that loads fine while the tags inside it silently fail — a scenario that breaks tracking without ever crashing the page, which makes it hard to spot without checking the console or the CSP reports.
The mistake that neutralizes the whole policy: reaching for unsafe-inline
Faced with a broken inline script (often the GTM configuration snippet pasted directly into <head>, or a hand-written onclick handler), the fastest fix is adding 'unsafe-inline' to script-src. That directive allows any inline script to run, including one an attacker managed to inject — which cancels out the exact XSS protection CSP is meant to provide. A study by Calzavara, Rabitti and Bugliesi ("Content Security Problems? Evaluating the Effectiveness of Content Security Policy in the Wild," CCS 2016) analyzed a large sample of CSP policies actually deployed across the web and found that most contained configuration errors that undid most of their effect — unsafe-inline being one of the most frequent causes. The correct alternative is a nonce (a random value generated on each request, added both to the CSP header and to the allowed <script> tag) or a hash of the inline script's exact content: more setup upfront, but a policy that actually protects against an injected script, since it will have neither the right nonce nor the right hash.
Implementing it on Next.js
On a Next.js project like the LanderKit templates, the header needs no middleware or dedicated API route — it's declared directly in next.config.ts via the headers() function: one entry with source: "/(.*)" applies the Content-Security-Policy header to every route, with the directive string (default-src 'self'; script-src 'self' *.googletagmanager.com js.stripe.com; frame-src js.stripe.com checkout.stripe.com; connect-src 'self' *.google-analytics.com api.stripe.com; …) built from the table above. For a fully static page — the case for every /templates/[slug] route on this site, covered in the article on Next.js/Vercel caching strategy — this header is set once at build time and served as-is from the CDN, with no per-request compute cost: CSP adds no perceptible latency, despite what its technical-sounding name might suggest.
Testing without breaking anything: report-only mode
Shipping a strict CSP straight to production is a bet that no legitimate script will get blocked by mistake — a risky bet on a page whose conversion rate depends precisely on advertising tracking working correctly. The Content-Security-Policy-Report-Only header solves that: it applies the exact same rules but blocks nothing — it simply logs, via report-to, every violation that would have occurred. Running that version for a few days of real traffic surfaces the domains missing from the initial table, before switching to the enforcing header once the list has settled.
The risk CSP doesn't cover: trust placed in the script itself
Allowlisting a domain in script-src guarantees nothing about the content that domain actually serves: if a third-party reviews widget's CDN gets compromised, or the provider updates its script to include unwanted code, CSP does nothing about it as long as the domain stays on the allowlist — it protects against an unknown script, not against a trusted script that turned malicious. A study by Nikiforakis et al. ("You Are What You Include: Large-scale Evaluation of Remote JavaScript Inclusions," CCS 2012), run across more than three million pages, found that a significant share of the web's most-visited sites trust script providers whose maintenance infrastructure had exploitable weaknesses. The practical takeaway for a landing page: keeping the number of embedded third-party scripts to the strict minimum — already the point of the article on GTM and the threshold where it becomes worth it — is a security measure in its own right, not just a performance habit.
A well-set CSP never shows up in a landing page's design, much like a solid caching strategy or a correctly set Cache-Control: it doesn't convert anyone on its own, but it turns a compromised third-party script from a potential incident into a single blocked line in a console. LanderKit templates ($89 each, $229 for the pack of 10) ship as static Next.js projects, built to be deployed to Vercel with a next.config.ts already in place — adding a CSP header tailored to a project's own tracking stack takes a list of domains, not a rewrite.
FAQ
Frequently asked questions
Will a CSP block Google Tag Manager or the Meta pixel by default?
Yes, if the matching domain isn't explicitly added to script-src (and connect-src for the network calls they trigger). That's why it's worth starting in Content-Security-Policy-Report-Only mode: the policy runs without blocking anything and surfaces the missing domains before switching to the enforcing version.
Why not just add 'unsafe-inline' to avoid configuration errors?
Because that directive allows any inline script to run, including one an attacker injects through an XSS flaw — which cancels out the main protection a CSP is meant to provide. A nonce or a hash takes a bit more setup but keeps the policy actually protective.
Does a landing page with no member area really need a CSP?
Yes, as soon as it includes a form or a widget that displays dynamic content (customer reviews, comments): those are theoretical entry points for script injection. CSP is defense in depth that catches what input validation would have let through, not a protection reserved for sites with user accounts.
Where should the Content-Security-Policy header be declared on a Next.js project?
The simplest place is the headers() function in next.config.ts, with a rule applied to every route (source: "/(.*)"). For a static page, the header is set once at build time and served from the CDN with no per-request compute cost.
Read next
Related articles
- Server-side Tag Manager (sGTM) on a landing page: is it actually worth itA Google Tag Manager container that runs on a server instead of the visitor's browser: that's the promise of server-side tag management, sold as the fix for ad blockers and Safari's ITP. Research has found data leaks even on the server side, though, and the infrastructure has a cost that rarely makes it into the sales pitch. What actually justifies the move — and what doesn't need it.
- The canonical tag on a landing page: why and how to use itA single landing page often ends up with several variants: one URL per ad source, one per city, one per ad group, sometimes one per A/B test. To a visitor, these are different pages. To Google, they're near-duplicates — and without a canonical tag, the search engine decides on its own which one to index.
- The Speculation Rules API: making the click on a landing page's CTA truly instantA visitor clicks "Book a demo" or "Join the waitlist," and waits a beat while the next page loads — a few hundred milliseconds, enough to introduce a moment of hesitation. Next.js's prefetch already downloads that page's data ahead of time, but not its render. Chrome's Speculation Rules API goes a step further: it executes and paints the target page in the background before the click happens, so the navigation feels like it takes no time at all.