Tous les articles

Comment mettre en place un nonce CSP, par requete, avec Express, Next.js et nginx

CentralCSP Team ·

Dernière mise à jour:

Un nonce vous permet d'autoriser des scripts inline précis sous une politique de sécurité du contenu (CSP) sans tous les réactiver. Une CSP est un header de réponse HTTP qui indique au navigateur quels scripts il peut exécuter, et par défaut elle bloque les scripts inline. Un nonce est un jeton aléatoire frais que vous placez à la fois dans le header et sur chaque <script> inline auquel vous faites confiance ; le navigateur n'exécute que les scripts inline dont l'attribut nonce correspond à la valeur du header.

En résumé : générez un nonce unique et crypto-aléatoire sur chaque réponse, placez-le dans script-src sous la forme 'nonce-...', et répétez-le dans l'attribut nonce de chaque script inline de confiance. Le piège est que le nonce doit changer à chaque réponse et provenir du code applicatif qui génère le HTML, ce qui explique pourquoi un serveur web statique comme nginx ne peut pas le faire seul.

Vous débutez avec la CSP ? Premiers pas avec la politique de sécurité du contenu couvre les bases, et pourquoi ne jamais utiliser unsafe-inline en CSP explique pourquoi un nonce vaut mieux que 'unsafe-inline'.

Ce qu'est un nonce et les deux règles

Un nonce ("number used once") est une courte valeur aléatoire que le serveur génère par réponse. Il apparaît dans le header de la politique et sur chaque script inline de confiance. Comme la valeur est imprévisible et change à chaque fois, un attaquant qui injecte un script inline ne peut pas fournir le bon nonce, donc le script injecté reste bloqué pendant que le vôtre s'exécute.

Deux règles rendent un nonce sûr :

  • Unique par réponse. Générez un nouveau nonce à chaque réponse HTTP, n'en réutilisez jamais un d'une requête à l'autre. Un nonce réutilisé peut être lu sur une page et rejoué par un attaquant, ce qui en annule l'intérêt.
  • Cryptographiquement aléatoire. Générez-le depuis une source sûre avec assez d'entropie (16 octets, 128 bits, est la recommandation courante). N'utilisez jamais Math.random() ; il est prévisible.

Générer le nonce

En Node.js, le module intégré crypto vous donne une valeur sûre. Seize octets aléatoires, encodés en base64, est le standard :

const crypto = require("crypto");
const nonce = crypto.randomBytes(16).toString("base64");

Générez-le une fois par requête, puis utilisez la même valeur dans le header et dans le template de cette réponse.

Associer le nonce à 'strict-dynamic'

Un nonce seul ne fait confiance qu'aux balises exactes sur lesquelles vous le placez. Dès qu'un script de confiance en injecte un autre (un loader d'analytics qui ajoute un tracker, un tag manager qui écrit d'autres balises de script), le script injecté n'a pas de nonce et est bloqué. Ajouter 'strict-dynamic' indique au navigateur de propager la confiance d'un script nonçé vers les scripts qu'il charge, de sorte qu'un loader de confiance peut récupérer ce dont il a besoin sans allowlist d'hôtes.

Content-Security-Policy: script-src 'nonce-r4nd0m' 'strict-dynamic'; object-src 'none'; base-uri 'none'

Utilisez un nonce avec 'strict-dynamic' comme forme par défaut d'une politique stricte.

Express

Dans Express, générez le nonce dans un middleware, stockez-le sur res.locals pour que vos templates puissent le lire, puis définissez le header. Helmet accepte une fonction comme valeur de directive, qui s'exécute par réponse et est l'endroit propre pour lire le nonce :

const crypto = require("crypto");
const helmet = require("helmet");

app.use((req, res, next) => {
  res.locals.nonce = crypto.randomBytes(16).toString("base64");
  next();
});

app.use(
  helmet({
    contentSecurityPolicy: {
      directives: {
        "script-src": [
          (req, res) => `'nonce-${res.locals.nonce}'`,
          "'strict-dynamic'",
        ],
        "object-src": ["'none'"],
        "base-uri": ["'none'"],
      },
    },
  })
);

Émettez ensuite le même nonce dans le template pour chaque script inline de confiance :

<script nonce="<%= nonce %>">
  // your trusted inline script
</script>

Comme Helmet appelle la fonction à chaque réponse et que le middleware définit un res.locals.nonce frais à chaque fois, le header et le markup portent toujours la même valeur par requête.

Next.js

Next.js génère le nonce dans un middleware et le transmet à la couche de rendu, donc la configuration de l'App Router comporte quelques pièces mobiles (lire le nonce dans un Server Component, le propager vers les propres scripts inline du framework). C'est suffisant pour mériter son propre tutoriel.

Pour la configuration complète de l'App Router de bout en bout, voyez comment mettre en place un nonce CSP dans Next.js. Les mêmes règles par réponse et crypto-aléatoires ci-dessus s'appliquent ; l'article couvre le branchement du nonce à travers le middleware et jusque dans le rendu produit.

nginx

nginx sert des réponses mais ne génère pas votre HTML, il ne peut donc pas, à lui seul, placer un nonce concordant à la fois dans le header et dans chaque balise <script> de la même requête. Un nonce ne fonctionne que lorsque le même code qui écrit le markup écrit aussi la valeur du header. Si nginx sert de reverse proxy à une application que vous contrôlez, la bonne réponse est de générer le nonce dans cette application (Express, Next.js, votre framework backend) et de laisser nginx faire passer la réponse sans la modifier.

L'exception est un site purement statique, sans backend pour le faire. Là, vous pouvez placer un placeholder impossible à deviner dans votre HTML et faire en sorte que nginx génère un $request_id frais par requête et remplace le placeholder par cette valeur avec sub_filter, de sorte que le header et les balises portent la même valeur. Cela s'accompagne de vraies contraintes (le placeholder doit être impossible à deviner, sub_filter ne peut pas toucher un corps compressé, et l'héritage de add_header se casse facilement), donc cela a son propre guide : comment ajouter un nonce CSP frais à un site statique avec nginx. Si vos scripts inline ne changent jamais, un hash CSP est encore plus simple à la couche nginx, car un hash n'a pas besoin d'une génération par requête.

Pour définir le header lui-même selon les stacks, voyez comment définir un header CSP dans chaque framework.

Visualisez-le dans vos reports

Un nonce qui ne correspond pas (une valeur périmée, un script que vous avez oublié de marquer) apparaît comme un script inline bloqué. Déployez d'abord la politique en Report-Only pour que le navigateur signale ces blocages au lieu de casser la page. CentralCSP collecte ces reports issus du trafic réel et les regroupe, pour que vous puissiez confirmer que chaque script inline de confiance est marqué avant de passer en enforcement. Vous pouvez démarrer un essai gratuit et y pointer un header Report-Only.

Pour la référence complète des directives et des mots-clés, voyez la référence de la politique CSP.

Des lignes de violation dont l'origine bloquée indique inline plutôt qu'un hôte

Questions fréquentes

nginx peut-il générer un nonce CSP ?

Pas tant qu'il sert de proxy à une application, car nginx ne génère pas votre HTML et ne peut pas placer un nonce concordant à la fois dans le header et dans chaque script inline de cette requête. Générez plutôt le nonce dans l'application. Pour un site purement statique, vous pouvez utiliser un placeholder impossible à deviner avec $request_id et sub_filter, décrit dans comment ajouter un nonce CSP frais à un site statique avec nginx.

Quelle doit être la longueur d'un nonce CSP ?

Générez-le depuis une source aléatoire sûre avec au moins 128 bits (16 octets) d'entropie, encodés en base64. La longueur exacte importe moins que le fait qu'il soit imprévisible et unique par réponse.

Ai-je besoin de 'strict-dynamic' avec un nonce ?

Vous en avez besoin quand un script de confiance injecte d'autres scripts (un tag manager ou un loader d'analytics). 'strict-dynamic' propage la confiance d'un script nonçé vers les scripts qu'il charge, pour qu'ils ne soient pas bloqués faute de nonce.

Sources

À lire aussi