Google Tag Manager sous une CSP stricte dans Next.js
CentralCSP Team ·
Dernière mise à jour:
Vous pouvez faire tourner Google Tag Manager (GTM) dans une app Next.js App Router sous une politique de sécurité du contenu (CSP) stricte sans ajouter aucun domaine Google à votre directive de script. Une CSP est un header de réponse HTTP qui indique au navigateur quels scripts et autres ressources une page peut charger et exécuter. L'approche stricte est un nonce par requête que Next.js génère, copié sur le script d'amorçage GTM, avec 'strict-dynamic' qui propage cette confiance à gtm.js et à chaque tag que GTM charge.
C'est le complément spécifique à Next.js de deux articles que vous devriez lire d'abord. Comment mettre en place un nonce CSP dans Next.js couvre le câblage de base du nonce dans proxy.ts, et CSP pour Google Analytics et Tag Manager couvre le mécanisme indépendant du framework : pourquoi le nonce sur l'amorçage est ce qui compte, et où les hosts Google ont leur place. Cet article relie les deux et souligne les pièges propres à Next.js.
Le flux en une minute
- Générez un nonce par requête dans
proxy.tset placez-le dans le header CSP sur la requête et sur la réponse. - Lisez ce nonce dans le layout et mettez-le sur le script d'amorçage GTM.
'strict-dynamic'fait confiance à l'amorçage, puis àgtm.jset aux tags que GTM injecte, parce qu'ils descendent d'un script porteur de nonce.- Gardez les hosts Google hors de
script-src. Ils vont dansconnect-src,img-srcetframe-src.
Les trois premières étapes sont de la pure plomberie Next.js. La quatrième est la partie que les gens ratent.
Le nonce vient du proxy
Dans une app Next.js App Router, vous générez un nonce frais par requête dans proxy.ts (la fonction proxy exportée, renommée depuis middleware.ts dans Next.js 16 et versions ultérieures) avec Web Crypto, vous placez la CSP sur les headers de la requête transmise pour que Next.js puisse lire le nonce, et vous la replacez sur la réponse pour que le navigateur l'impose. Cette configuration est couverte de bout en bout dans comment mettre en place un nonce CSP dans Next.js, donc voici seulement la partie qui compte pour GTM :
const nonce = Buffer.from(crypto.randomUUID()).toString('base64')
const csp = `script-src 'self' 'nonce-${nonce}' 'strict-dynamic'` // [!code highlight]
// set csp on requestHeaders and on the response, plus 'x-nonce' for your componentsNext.js applique ce nonce aux scripts qu'il rend pour vous. La seule chose qu'il ne fait pas automatiquement, c'est mettre le nonce sur un amorçage tiers que vous écrivez à la main, comme GTM. C'est l'étape qui vous revient.
Lisez le nonce et mettez-le sur l'amorçage GTM
GTM fonctionne en injectant une balise <script> pour gtm.js, qui injecte ensuite d'autres scripts pour chaque tag de votre conteneur. Sous une politique stricte, le navigateur n'exécutera l'amorçage que s'il porte le nonce correspondant. Une fois que c'est le cas, 'strict-dynamic' fait confiance à l'amorçage et à tout ce qu'il charge en aval, donc gtm.js et les tags tournent sans aucun host Google dans script-src.
Lisez le nonce depuis les headers de la requête dans votre layout racine (un Server Component), puis rendez l'amorçage de Google qui propage le nonce avec cette valeur :
import { headers } from 'next/headers'
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const nonce = (await headers()).get('x-nonce') ?? ''
return (
<html lang="en">
<head>
<script
nonce={nonce}
dangerouslySetInnerHTML={{
__html: `(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;
var n=d.querySelector('[nonce]');
n&&j.setAttribute('nonce',n.nonce||n.getAttribute('nonce'));
f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-XXXXXX');`,
}}
/>
</head>
<body>{children}</body>
</html>
)
}Deux détails rendent ceci compatible avec une CSP stricte. L'attribut nonce={nonce} est ce qui permet à 'strict-dynamic' de faire confiance à l'amorçage inline. Les lignes d.querySelector('[nonce]') copient le nonce sur la balise gtm.js injectée, ce qui maintient le nonce en circulation pour les tags qui le cherchent. Le nonce ici doit être la valeur exacte que le proxy a mise dans le header CSP, c'est pourquoi vous le lisez depuis x-nonce plutôt que d'en générer un nouveau. Cet amorçage est le snippet officiel de Google conscient du nonce ; la seule partie propre à Next.js est la lecture du nonce depuis headers().
Lire headers() fait basculer la route en rendu dynamique, dont un nonce par requête a de toute façon besoin. Un nonce réutilisé sur une réponse mise en cache statiquement met la politique en échec.
Le piège d'ordonnancement de next/script
Il est tentant de charger GTM avec next/script au lieu d'une balise inline, et next/script accepte bien une prop nonce. Le hic, c'est l'ordre et le runtime.
next/script avec la stratégie par défaut afterInteractive injecte la balise depuis un client component après l'hydratation. Le nonce que vous passez doit être la même valeur par requête venue du proxy, donc vous devez quand même le lire depuis headers() dans un Server Component et le faire descendre en prop. Si vous générez ou codez en dur un nonce côté client, il ne correspondra pas au header et le script est bloqué. L'amorçage inline dans le layout évite cela entièrement parce qu'il est rendu côté serveur avec le nonce de la requête déjà en portée.
Si vous utilisez bien next/script, mettez strategy="afterInteractive" (pas beforeInteractive, qui s'exécute avant que le balisage porteur de nonce soit en place de façon fiable pour les tiers) et passez le nonce lu côté serveur :
import Script from 'next/script'
import { headers } from 'next/headers'
const nonce = (await headers()).get('x-nonce') ?? ''
<Script id="gtm" nonce={nonce} strategy="afterInteractive">{`/* GTM bootstrap here */`}</Script>Pour la plupart des configurations, l'amorçage inline rendu côté serveur est plus simple et moins sujet aux erreurs. Choisissez-en un, pas les deux, sinon vous chargez le conteneur deux fois.
Où vont réellement les hosts Google
'strict-dynamic' ne régit que les scripts. GTM et les tags qu'il charge envoient aussi des beacons d'analytics, récupèrent des pixels et ouvrent une frame de debug, et aucun de ces éléments n'est un chargement de script. Ces requêtes sont régies par d'autres directives, et elles ont toujours besoin que les hosts Google soient listés. C'est la partie qui fait trébucher la configuration Next.js autant que n'importe quelle autre stack, parce que le script-src strict est correct et la page casse quand même tant que vous ne remplissez pas ces directives.
connect-srcpour les beacons et fetches d'analytics. Utilisez des wildcards pour que les endpoints de collecte régionaux soient autorisés :https://*.googletagmanager.com https://*.google-analytics.com https://*.analytics.google.com https://www.google.com.img-srcpour les requêtes de pixels :https://*.google-analytics.com https://*.googletagmanager.com.frame-srcpour la frame Preview et Debug de GTM :https://www.googletagmanager.com.
Une politique stricte complète depuis le proxy, avec script-src ne portant que le nonce et 'strict-dynamic' :
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{SERVER-GENERATED-NONCE}' 'strict-dynamic';
connect-src 'self' https://*.googletagmanager.com https://*.google-analytics.com https://*.analytics.google.com https://www.google.com;
img-src 'self' https://*.google-analytics.com https://*.googletagmanager.com;
frame-src https://www.googletagmanager.com;
object-src 'none';
base-uri 'none';
report-to csp-endpointLes tags qui déclenchent des pixels de publicité ou de conversion (Google Ads, Floodlight) atteignent plus de hosts. N'ajoutez que ceux que votre conteneur déclenche réellement, que Report-Only vous montrera, plutôt que d'allowlister tout l'ensemble d'avance.
Testez en Report-Only d'abord
Un conteneur GTM change dès que quelqu'un ajoute un tag, et chaque tag peut atteindre un host que votre politique n'a pas autorisé. Déployez la politique d'abord sur le header Content-Security-Policy-Report-Only en changeant le nom du header dans le proxy et en ajoutant un header Reporting-Endpoints :
Reporting-Endpoints: csp-endpoint="https://<Endpoint-ID>.report.centralcsp.com"Le navigateur ne bloque rien et signale tout ce qu'il aurait bloqué, donc vous voyez le host exact dont un nouveau tag a besoin avant qu'il ne puisse casser l'analytics en production.
CentralCSP collecte ces reports Report-Only, les regroupe par directive et origine, et inventorie les scripts qui tournent sur chaque page, pour que vous voyiez précisément quel host Google un tag veut et quels scripts GTM charge avant d'imposer. Vous pouvez démarrer un essai gratuit, pointer un header Report-Only depuis votre app Next.js dessus, et regarder les reports arriver depuis du trafic réel.
Questions fréquentes
Dois-je ajouter googletagmanager.com à script-src dans Next.js ?
Non. Avec un nonce par requête et 'strict-dynamic' dans script-src, le navigateur ignore les allowlists de hosts pour les scripts. Vous mettez le nonce sur l'amorçage GTM, et 'strict-dynamic' fait confiance à gtm.js et aux tags qu'il charge.
Comment Next.js passe-t-il le nonce à GTM ?
Pas automatiquement. Next.js met un nonce sur ses propres scripts, mais un amorçage tiers que vous écrivez, c'est à vous de le gérer. Lisez le nonce depuis headers() (le header x-nonce que le proxy a posé) dans votre layout et mettez-le sur le script d'amorçage GTM.
Dois-je charger GTM avec next/script ou un script inline ?
Les deux marchent sous une CSP stricte, mais vous devez passer le nonce par requête venu du proxy dans les deux cas. L'amorçage inline rendu côté serveur est plus simple parce que le nonce est déjà en portée ; next/script a besoin que le nonce descende en prop et de la stratégie afterInteractive. N'utilisez pas les deux, sinon le conteneur se charge deux fois.
Pourquoi GTM casse-t-il encore avec un script-src correct ?
Parce que GTM et ses tags envoient aussi des beacons, récupèrent des pixels et ouvrent une frame de debug, que script-src ne régit pas. Ajoutez les hosts Google à connect-src, img-src et frame-src.
À retenir
Faire tourner GTM sous une CSP stricte dans Next.js, c'est la configuration de base du nonce plus une étape supplémentaire. Générez le nonce dans proxy.ts, lisez-le depuis headers() dans le layout, et mettez-le sur l'amorçage GTM pour que 'strict-dynamic' fasse confiance à gtm.js et à chaque tag. Gardez les hosts Google hors de script-src, où ils sont ignorés, et listez-les dans connect-src, img-src et frame-src, où les requêtes se produisent réellement. Testez en Report-Only d'abord pour qu'un nouveau tag ne casse jamais l'analytics en production.
Pour aller plus loin : le guide CSP de Next.js et les recommandations GTM et CSP de Google.
Sources
- Next.js, guide Content Security Policy
- Next.js, composant Script
- Google, utiliser une CSP avec Tag Manager
- W3C, Content Security Policy Level 3