LanderKit

Templates written in French — fully translatable in minutes

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.

Domains to add per tool (minimum)
ToolDirectiveDomains
Google Tag Managerscript-src*.googletagmanager.com
GA4 (loaded via GTM)connect-src*.google-analytics.com, *.analytics.google.com
Meta Pixelscript-src, connect-srcconnect.facebook.net, *.facebook.com
Google Ads (conversion)script-src, connect-src*.googleadservices.com, *.google.com
Stripe (Checkout)script-src, frame-src, connect-srcjs.stripe.com, checkout.stripe.com, api.stripe.com
Google Fonts (if not self-hosted)style-src, font-srcfonts.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