Tous les articles

Mettre en place un nonce CSP dans Next.js

CentralCSP Team ·

Dernière mise à jour:

Vous pouvez donner à une application Next.js App Router une Content Security Policy (CSP) stricte, basée sur un nonce, dans un seul fichier. 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. La façon propre de procéder dans Next.js consiste à générer un nouveau nonce par requête dans proxy.ts, à définir la politique à la fois sur la requête et sur la réponse, et à laisser Next.js appliquer ce nonce à chaque script qu'il rend pour vous.

Ce guide pratique cible le Next.js actuel (16). Il utilise Web Crypto pour que le code tourne dans le runtime Edge, définit un seul script-src strict avec un nonce et 'strict-dynamic', et montre comment lire le nonce quand vous avez votre propre script inline ou une balise next/script. Si les nonces sont nouveaux pour vous, démarrer avec Content Security Policy explique ce qu'est un nonce et pourquoi il fonctionne.

La version courte

  1. Générez un nonce par requête dans proxy.ts avec Web Crypto.
  2. Définissez la CSP sur les headers de la requête transmise (pour que Next.js puisse lire le nonce) et sur la réponse (pour que le navigateur l'applique).
  3. Laissez Next.js ajouter automatiquement le nonce aux scripts qu'il rend. Vous ne mettez pas vous-même le nonce sur ses balises de script.
  4. Forcez le rendu dynamique sur toute route qui utilise le nonce. Un nonce par requête et l'optimisation statique sont incompatibles.
  5. Lisez le nonce depuis headers() dans un Server Component uniquement quand vous avez votre propre script inline ou une balise next/script.

Une note de version avant le code

Dans Next.js 16, la convention de fichier du middleware a été renommée. Le fichier est désormais proxy.ts et la fonction exportée est proxy. Le guide CSP officiel livre proxy.ts, donc c'est ce que ce billet utilise.

Sur Next.js 15 et antérieur, ce fichier est middleware.ts avec export function middleware. Le corps est identique, seuls le nom du fichier et le nom de la fonction diffèrent.

Étape 1, générer le nonce dans proxy.ts

Le proxy tourne dans le runtime Edge, donc utilisez Web Crypto. C'est le détail qui fait trébucher : le crypto.randomBytes de Node et require('crypto') ne sont pas disponibles dans le runtime Edge, donc les appeler lève une erreur. Utilisez crypto.randomUUID(), qui fait partie de Web Crypto et y est disponible :

const nonce = Buffer.from(crypto.randomUUID()).toString('base64')

Un nonce doit être unique par requête, imprévisible, et en base64. crypto.randomUUID() vous donne une valeur fraîche et indevinable à chaque requête depuis une source cryptographiquement sûre, ce qui est exactement ce dont un nonce a besoin. Les exigences plus profondes (pourquoi il doit être par requête et issu d'une source aléatoire sûre) sont dans démarrer avec Content Security Policy, donc ce billet ne les redémontre pas.

Étape 2, écrire le proxy

Voici le proxy actuel complet. Il construit une politique stricte, y inscrit le nonce, et définit la politique à deux endroits :

// proxy.ts
import { NextRequest, NextResponse } from 'next/server'

export function proxy(request: NextRequest) {
  const nonce = Buffer.from(crypto.randomUUID()).toString('base64')
  const isDev = process.env.NODE_ENV === 'development'
  const cspHeader = `
    default-src 'self';
    script-src 'self' 'nonce-${nonce}' 'strict-dynamic'${isDev ? " 'unsafe-eval'" : ''};
    style-src 'self' 'nonce-${nonce}';
    img-src 'self' blob: data:;
    font-src 'self';
    object-src 'none';
    base-uri 'self';
    form-action 'self';
    frame-ancestors 'none';
    upgrade-insecure-requests;
  `
  const contentSecurityPolicyHeaderValue = cspHeader.replace(/\s{2,}/g, ' ').trim()

  const requestHeaders = new Headers(request.headers)
  requestHeaders.set('x-nonce', nonce)
  requestHeaders.set('Content-Security-Policy', contentSecurityPolicyHeaderValue)

  const response = NextResponse.next({ request: { headers: requestHeaders } })
  response.headers.set('Content-Security-Policy', contentSecurityPolicyHeaderValue)
  return response
}

Deux choses dans ce code font le vrai travail.

La politique est définie à la fois sur les headers de la requête transmise et sur la réponse. La définir sur la requête (requestHeaders.set('Content-Security-Policy', ...)) est la façon dont Next.js voit le nonce et l'applique aux scripts qu'il rend. La définir sur la réponse (response.headers.set('Content-Security-Policy', ...)) est ce que le navigateur applique réellement. Vous avez besoin des deux.

Le header x-nonce est un confort. Le proxy place le nonce brut sur un header de requête personnalisé pour que vos propres composants puissent le relire plus tard sans analyser la chaîne de la CSP.

'unsafe-eval' n'est ajouté qu'en développement. React utilise eval en développement, donc sans lui le serveur de dev casse sous la politique. La vérification isDev garde 'unsafe-eval' entièrement hors de la production, où il réactiverait l'exécution de chaîne en code et affaiblirait la politique.

La politique ici s'appuie sur 'strict-dynamic' plutôt que sur des listes d'hôtes autorisés pour les scripts. 'strict-dynamic' indique au navigateur de faire confiance aux scripts qui portent le nonce, ainsi qu'à tout script que ceux-ci chargent, et d'ignorer les listes d'hôtes pour les scripts. C'est ce qui rend une politique à nonce solide : un attaquant qui injecte une balise de script ne peut pas deviner le nonce, donc elle ne s'exécute jamais. Évitez de recourir à 'unsafe-inline' pour faire taire les erreurs de script ; il réactive exactement l'exécution inline que la CSP existe pour bloquer, et le navigateur l'ignore de toute façon dès qu'un nonce est présent.

Cadrez le proxy pour qu'il ignore les assets statiques

Vous ne voulez généralement pas que le proxy génère un nonce pour les fichiers statiques et les requêtes de prefetch. Ajoutez un config.matcher pour le cadrer sur les chemins qui rendent du HTML. Le guide officiel livre un matcher qui ignore les internes de Next.js, les fichiers statiques et les prefetches ; gardez-en un pour que le proxy ne tourne que là où le nonce est nécessaire.

Étape 3, laisser Next.js ajouter le nonce à ses propres scripts

C'est la partie qui rend Next.js agréable à utiliser. Next.js lit le nonce depuis le header Content-Security-Policy de la requête et l'applique automatiquement aux scripts qu'il rend. Cela couvre les scripts du framework, vos bundles de page, les scripts et styles inline que Next.js génère, et tout composant <Script nonce>.

Vous n'ajoutez donc pas manuellement un attribut nonce aux propres balises de script de Next.js. Définir la politique sur le header de requête à l'étape 2 est tout le câblage. Next.js fait le reste.

Étape 4, forcer le rendu dynamique sur les routes qui utilisent le nonce

Un nonce par requête n'a de sens que si chaque requête obtient son propre HTML rendu. Cela signifie que la route doit se rendre dynamiquement. L'optimisation statique, l'Incremental Static Regeneration (ISR) et le Partial Prerendering (PPR) sont incompatibles avec une CSP basée sur un nonce, parce qu'ils réutilisent une réponse pré-rendue sur plusieurs requêtes, et un nonce réutilisé ruine l'intérêt.

Forcez le rendu dynamique avec await connection() dans la page :

import { connection } from 'next/server'

export default async function Page() {
  await connection()
  // ...
}

Lire headers() dans la route (étape 5) la fait aussi passer en rendu dynamique, donc si vous y lisez déjà le nonce, vous n'aurez peut-être pas besoin de connection() en plus. Utilisez connection() pour les routes qui ont besoin du rendu dynamique mais qui ne touchent autrement pas aux données de la requête.

Étape 5, lire le nonce pour votre propre script inline

La plupart du temps, vous n'avez pas à toucher au nonce, parce que Next.js l'applique pour vous. Vous ne le lisez que quand vous avez votre propre script inline ou une balise next/script que Next.js ne nonce pas automatiquement.

Lisez-le depuis les headers de la requête dans un Server Component. Notez que headers() est asynchrone dans le Next.js actuel, donc vous l'attendez avec await :

import { headers } from 'next/headers'
import Script from 'next/script'

export default async function Page() {
  const nonce = (await headers()).get('x-nonce')
  return (
    <Script
      src="https://api-next.centralcsp.com/script.js"
      strategy="afterInteractive"
      nonce={nonce}
    />
  )
}

Le header x-nonce est celui que le proxy a défini à l'étape 2. Passez cette valeur à la prop nonce et le script est approuvé par la politique.

Pages Router

Si vous êtes encore sur le Pages Router, le proxy et le flux du nonce sont les mêmes. Ce qui diffère, c'est la façon de lire le nonce, parce qu'il n'y a pas de next/headers.

Dans une page, lisez-le dans getServerSideProps depuis les headers de la requête et passez-le en prop :

export async function getServerSideProps({ req }) {
  const nonce = req.headers['x-nonce'] ?? ''
  return { props: { nonce } }
}

Pour ajouter le nonce aux scripts au niveau du document, lisez-le dans _document.tsx et appliquez-le à <Head> et <NextScript> :

import Document, { Head, Html, Main, NextScript } from 'next/document'

class MyDocument extends Document {
  static async getInitialProps(ctx) {
    const initialProps = await Document.getInitialProps(ctx)
    const nonce = ctx.req?.headers?.['x-nonce'] ?? ''
    return { ...initialProps, nonce }
  }

  render() {
    const { nonce } = this.props as { nonce: string }
    return (
      <Html>
        <Head nonce={nonce} />
        <body>
          <Main />
          <NextScript nonce={nonce} />
        </body>
      </Html>
    )
  }
}

export default MyDocument

L'App Router est le focus de ce billet, donc voici la version courte. Le proxy de l'étape 2 reste inchangé.

Testez d'abord en Report-Only, puis surveillez les reports

Déployez la politique sur le header Content-Security-Policy-Report-Only avant de l'appliquer. En mode report-only, le navigateur ne bloque rien et signale seulement ce que la politique aurait bloqué, donc un script tiers oublié ne peut pas casser la page pendant que vous l'ajustez. Pointez la politique vers un endpoint de reporting et collectez depuis le trafic réel :

Reporting-Endpoints: csp-endpoint="https://<Endpoint-ID>.report.centralcsp.com"
Content-Security-Policy-Report-Only:
  default-src 'self';
  script-src 'self' 'nonce-r4nd0mBase64Value' 'strict-dynamic';
  style-src 'self' 'nonce-r4nd0mBase64Value';
  object-src 'none';
  base-uri 'self';
  frame-ancestors 'none';
  report-to csp-endpoint

Pour définir ceci sur le header report-only dans Next.js, remplacez Content-Security-Policy par Content-Security-Policy-Report-Only dans le proxy, ajoutez un header Reporting-Endpoints, et ajoutez report-to csp-endpoint à la chaîne de la politique.

CentralCSP collecte ces reports de violation en Report-Only, les regroupe par directive et par origine, et montre les scripts qui tournent sur chaque page, pour que vous voyiez exactement de quel tiers une balise a besoin avant d'appliquer. Vous pouvez démarrer un essai gratuit, pointer un header Report-Only vers lui, et regarder les reports arriver. Pour évaluer la politique finie à la recherche de sources faibles ou d'un object-src manquant, passez-la dans le évaluateur CSP, et pour vérifier ce qu'un site en ligne envoie déjà, utilisez le scanner CSP. Le workflow complet Report-Only d'abord, sur tous les frameworks, est dans comment construire une CSP solide.

Faire tourner Google Tag Manager ou GA4 dans votre app Next.js sous cette politique fonctionne de la même manière : ajoutez le nonce au bootstrap GTM et laissez 'strict-dynamic' faire confiance aux tags. Voyez CSP avec Google Analytics et Tag Manager pour le snippet exact et l'endroit où placer les hôtes Google.

Questions fréquentes

Comment ajouter un nonce CSP dans Next.js ?

Générez un nonce par requête dans proxy.ts avec Buffer.from(crypto.randomUUID()).toString('base64'), définissez le header Content-Security-Policy à la fois sur la requête transmise et sur la réponse, et incluez 'nonce-...' et 'strict-dynamic' dans script-src. Next.js applique ensuite automatiquement le nonce aux scripts qu'il rend.

Pourquoi mon nonce lève-t-il une erreur dans le middleware ou le proxy Next.js ?

Parce que vous utilisez le crypto.randomBytes de Node ou require('crypto'), qui ne sont pas disponibles dans le runtime Edge où tourne le proxy. Utilisez Web Crypto à la place : crypto.randomUUID().

Dois-je ajouter un nonce à chaque balise de script dans Next.js ?

Non. Next.js lit le nonce depuis le header Content-Security-Policy de la requête et l'applique aux scripts qu'il rend, y compris les scripts du framework, les bundles de page, et les composants <Script nonce>. Vous ne lisez le nonce vous-même que pour votre propre script inline ou une balise next/script que vous contrôlez.

Pourquoi un nonce casse-t-il mes pages statiques dans Next.js ?

Un nonce par requête exige le rendu dynamique, il est donc incompatible avec l'optimisation statique, l'ISR et le Partial Prerendering. Forcez le rendu dynamique avec await connection(), ou lisez headers() dans la route, ce qui la fait aussi passer en rendu dynamique.

Est-ce différent sur Next.js 15 ?

Seulement le nom du fichier. Sur Next.js 15 et antérieur, le fichier est middleware.ts avec export function middleware. La génération du nonce, le double header requête et réponse, et le comportement d'auto-nonce sont identiques.

À retenir

Une CSP stricte basée sur un nonce dans Next.js se résume à un seul proxy : générez le nonce avec Web Crypto, définissez la politique à la fois sur la requête et sur la réponse, et laissez Next.js ajouter le nonce à ses propres scripts. Forcez le rendu dynamique sur les routes qui l'utilisent, lisez le nonce depuis headers() uniquement pour vos propres scripts inline, et déployez le tout d'abord en Report-Only pour que rien ne casse pendant que vous l'ajustez.

Pour aller plus loin : le guide CSP de Next.js, le guide CSP de MDN, et la spécification W3C CSP Level 3.