Ajouter un nonce CSP frais à un site statique avec nginx
CentralCSP Team ·
Dernière mise à jour:
Une politique de sécurité du contenu (CSP) stricte repose sur un nonce : une valeur aléatoire qui change à chaque réponse, envoyée dans le header Content-Security-Policy et répétée sur chaque balise <script> de confiance. Une application dynamique génère cette valeur pendant qu'elle rend la page. Un site statique n'a rien qui rende la page, et nginx n'a aucun moyen intégré de générer un nonce, d'où le conseil habituel : « on ne peut pas mettre de nonce sur un site statique ». Si, on peut. Vous placez un marqueur non devinable dans votre HTML et vous laissez nginx le remplacer par un nonce frais au moment de la sortie.
L'idée en une phrase : votre HTML est livré avec un marqueur fixe dans chaque <script nonce="...">, et à chaque requête nginx génère un nonce par requête, l'écrit dans le header CSP, et utilise sub_filter pour remplacer le marqueur dans le HTML par cette même valeur. Le même nonce dans le header et dans les balises, frais à chaque fois, avec du nginx simple.
Vous découvrez les nonces ? Comment mettre en place un nonce CSP, par requête couvre d'abord le concept à travers les frameworks applicatifs ; cet article est la version nginx-only, pour site statique.
Pourquoi un site statique a besoin d'un contournement
Un nonce CSP doit être unique par réponse et non devinable, et exactement la même valeur doit apparaître à deux endroits : la source script-src 'nonce-...' et chaque <script nonce="..."> de confiance. Si elles ne correspondent pas, le navigateur bloque le script.
Un framework applicatif fait cela naturellement : il crée une valeur aléatoire par requête et la dépose à la fois dans le header et dans le template. Un site statique n'a pas d'étape équivalente. Les fichiers sur le disque sont fixes, et nginx les sert tels quels. nginx n'a pas non plus de fonctionnalité native de nonce, vous devez donc fabriquer vous-même la valeur par requête et la coudre dans la réponse. sub_filter (un réécrivain léger du corps de réponse) associé au $request_id par requête de nginx vous donne les deux moitiés.
Étape 1 : placer un marqueur non devinable dans votre HTML
Choisissez un jeton aléatoire, une seule fois, et utilisez-le comme valeur de nonce sur chaque script de confiance de votre HTML statique. Générez-le avec n'importe quoi qui produit une longue chaîne aléatoire :
openssl rand -hex 16Supposons que cela vous donne 9f2c8a1b7e4d60359f2c8a1b7e4d6035. Utilisez-le comme marqueur partout où un script a besoin d'un nonce :
<script nonce="__csp_nonce_9f2c8a1b7e4d60359f2c8a1b7e4d6035__" src="/app.js"></script>
<script nonce="__csp_nonce_9f2c8a1b7e4d60359f2c8a1b7e4d6035__">init();</script>Le marqueur est une constante de build, identique dans votre HTML et dans votre configuration nginx. Ce n'est pas le nonce ; c'est le repère que nginx cherche et écrase.
Le marqueur doit être non devinable, et c'est une exigence de sécurité, pas un choix de style. Si vous utilisez une chaîne bien connue comme **CSP_NONCE** et que du contenu contrôlé par un attaquant peut atteindre le corps de la réponse avant que nginx n'exécute sub_filter, une valeur de requête réfléchie renvoyée dans une page, un champ de commentaire ou de profil intégré dans du HTML « statique », un server-side include, un CDN qui réécrit le corps, alors un attaquant peut injecter <script nonce="**CSP_NONCE**">evil()</script> et nginx apposera le vrai nonce valide sur son script. Son code devient de confiance et la protection du nonce disparaît. Un long jeton aléatoire qu'il ne peut pas deviner ferme cette porte. sub_filter retire le marqueur avant l'envoi de la réponse, il n'apparaît donc jamais dans le source de la page ; deviner est la seule voie d'entrée, ce qui est exactement ce qu'un jeton non devinable refuse. Un site entièrement statique sans contenu réfléchi présente un risque plus faible, mais le jeton ne coûte rien et garde la technique sûre si le site cesse un jour d'être parfaitement statique.
Ne placez le marqueur que sur vos propres scripts de confiance. Ne configurez pas nginx pour poser aveuglément un nonce sur chaque <script de la page (voir les pièges), car cela accorderait aussi la confiance à des scripts inline injectés.
Étape 2 : générer et injecter le nonce dans nginx
Trois directives font le travail : capturer une valeur par requête, réécrire le marqueur avec cette valeur, et envoyer le header avec la même valeur.
server {
listen 443 ssl;
root /var/www/static;
# A fresh, unique value per request
set $cspNonce $request_id;
location / {
# Replace the placeholder in every trusted tag with the nonce
sub_filter '__csp_nonce_9f2c8a1b7e4d60359f2c8a1b7e4d6035__' '$cspNonce';
sub_filter_once off;
# Send the matching nonce in the policy
add_header Content-Security-Policy "default-src 'self'; script-src 'nonce-$cspNonce' 'strict-dynamic'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'" always;
}
}$request_id est une variable de base de nginx : 16 octets aléatoires rendus en 32 caractères hexadécimaux, générés à neuf pour chaque requête, sans module supplémentaire. C'est une valeur de nonce valide : unique par réponse et imprévisible. sub_filter trouve le marqueur dans le corps HTML et le remplace par $cspNonce, et sub_filter_once off lui fait remplacer chaque occurrence plutôt que seulement la première. La ligne add_header place le même $cspNonce dans script-src comme source de nonce, aux côtés de 'strict-dynamic' pour qu'un script de confiance puisse charger le reste sans que vous listiez des hôtes.
Les pièges qui font mal
Ça fonctionne, mais une poignée de comportements de nginx le casseront silencieusement si vous les manquez.
sub_filtera besoin de son module. Il provient dengx_http_sub_module, intégré à la plupart des paquets de distribution. Si le vôtre en manque, nginx doit être compilé avec--with-http_sub_module.- Il ne réécrit que du HTML non compressé.
sub_filterne peut pas voir à l'intérieur d'un corps gzippé. Servir des fichiers statiques bruts convient, car nginx compresse la réponse après l'exécution desub_filter. Mais si vous servez des fichiers.gzpré-compressés avecgzip_static,sub_filterne voit jamais le balisage, et le marqueur n'est pas remplacé. Si vous placez un jour cela derrière un upstream proxifié, désactivez la compression upstream pour cet emplacement avecproxy_set_header Accept-Encoding "";. add_headern'hérite pas toujours. Si un bloclocationcontient unadd_headerqui lui est propre, il cesse d'hériter des directivesadd_headerdu blocserverouhttpenvironnant. Donc si votre CSP est définie plus haut et qu'un emplacement ajoute un autre header, la CSP disparaît discrètement. Définissez le header dans le même bloc, et utilisezalwayspour qu'il soit envoyé aussi sur les réponses en erreur, pas seulement sur les 2xx et 3xx.- Ne faites confiance qu'à vos propres balises. Le modèle du marqueur limite déjà la réécriture aux balises que vous avez marquées. Ne passez pas à la réécriture de chaque
<script, cela remettrait un nonce valide à n'importe quel script inline de la page, y compris un script injecté. $request_idest imprévisible, pas un tirage CSPRNG académique. Chaque worker nginx amorce une clé aléatoire une fois et dérive les request ids à partir d'elle, si bien que les valeurs sont de fait non devinables et uniques, ce qu'un nonce demande. Si vous voulez une valeur tirée directement d'un RNG cryptographique, utilisez les options njs ou OpenResty ci-dessous.
Associez-le à strict-dynamic
Le mot-clé 'strict-dynamic' de l'exemple est ce qui rend un nonce praticable sur un vrai site. Avec lui, vous ne mettez un nonce que sur vos scripts d'entrée ; tout script qu'ils chargent est approuvé automatiquement, et les allowlists d'hôtes (ainsi que 'unsafe-inline') sont ignorées. Sans lui, vous devriez ajouter le nonce à chaque balise de script, y compris celles que votre code injecte à l'exécution, que vous ne pouvez pas atteindre depuis un template statique. Comment fonctionne strict-dynamic couvre les compromis. Gardez object-src 'none' et base-uri 'none' à ses côtés, comme dans la configuration ci-dessus, et construisez le reste de la politique à partir d'un modèle de départ de CSP stricte.
Quand se tourner vers autre chose
La combinaison $request_id et sub_filter est la façon la plus rapide et sans dépendance de mettre un nonce sur un site statique. Deux situations appellent quelque chose de plus solide.
- Vous avez en réalité un backend. Si nginx fait du reverse-proxy vers une application que vous contrôlez, générez plutôt le nonce dans l'application. Elle rend déjà le HTML, elle peut donc écrire une seule valeur à la fois dans les balises
<script nonce>et dans le headerContent-Security-Policy, et nginx se contente de laisser passer le header. Pas de réécriture du corps, pas de surprises de compression. Mettre en place un nonce CSP par requête montre le modèle côté application pour Express et Next.js. - Vous voulez un nonce issu d'un RNG cryptographique et pouvez ajouter un module. Le module njs est livré avec nginx et peut générer un nonce base64 avec
crypto.getRandomValues, définir le header dansjs_header_filter, et réécrire le corps dansjs_body_filter. OpenResty avec Lua (resty.random.bytesplusngx.ctxpour partager la valeur entre les phases header et corps) fait la même chose. Les deux représentent plus de code quesub_filter, et les deux se heurtent à la même règle sur le fait de ne pas traiter les corps compressés.
Vérifiez ce que vous avez déployé
Après le déploiement, confirmez que le header et les balises correspondent bien sur une réponse en direct. Passez l'URL dans le scanner CSP pour lire le header que nginx envoie, et dans l'évaluateur CSP pour noter la politique et signaler un script-src faible. Chargez la page et vérifiez la console : un nonce qui ne correspond pas apparaît immédiatement comme une violation de script bloqué, ce qui est le moyen le plus rapide de repérer un marqueur qui n'a pas été remplacé.
Une vérification en console ne couvre que les pages que vous ouvrez vous-même. Comme un nonce qui ne correspond pas est signalé comme une csp-violation ordinaire, pointer la politique vers un endpoint CentralCSP transforme ce contrôle ponctuel en couverture continue : si une réponse en cache livre un jour un marqueur périmé, les violations arrivent depuis le trafic réel au lieu d'attendre que vous rechargiez la bonne page.
Sur le même sujet
- Comment mettre en place un nonce CSP, par requête, la version pour framework applicatif
- strict-dynamic expliqué
- Un modèle de départ de CSP à copier et resserrer
- Nonces et hashes, la référence