magentix

Développeur backend indépendant Web, e-commerce, Open-Source, low-tech, indie-web, slow-web, SSG, accessibility, PHP, Python

Billets : Utiliser l'attribut nonce dans la stratégie de sécurité du contenu d'un site


Utiliser l'attribut nonce dans la stratégie de sécurité du contenu d'un site

Par Matthieu le 26/09/2023

Avec CSP level 2 (Content-Security-Policy), il est possible d'utiliser un attribut nonce pour déterminer si le chargement d'un élément script ou style est autorisé ou non.


En analysant votre site sur PageSpeed Insights, vous avez peut être déjà vu ce type de message :

Les listes d'autorisation des hôtes peuvent souvent être contournées. Envisagez d'utiliser des nonces ou des hachages CSP à la place, ainsi que strict-dynamic, si nécessaire.

Le nonce est un attribut efficace pour sécuriser les scripts d'une page.

Recommandation CSP Nonce PageSpeed Insights

Principe

Dans l'en-tête HTTP, la directive content-security-policy contient pour script-src le "nonce" :

script-src 'strict-dynamic' 'nonce-SC3tB3UYXQNKc4c81B1q37sr'; 

En-tête HTTP avec attribut nonce pour script-src du CSP

Le nonce est unique, la valeur change à chaque rechargement de la page.

Les scripts "inline" de la page doivent indiquer la même valeur que le nonce de la directive CSP pour script-src :

<script type="text/javascript" nonce="SC3tB3UYXQNKc4c81B1q37sr">
    myFunction();
</script>

<script type="text/javascript">
    myNotExecutedFunction();
</script>

La valeur strict-dynamic permet de charger des scripts supplémentaires, même externes, sans devoir systématiquement indiquer le domaine dans la liste des autorisations.

<script type="text/javascript" nonce="SC3tB3UYXQNKc4c81B1q37sr" src="https://my.cdn.com/app.js"></script>

Le nonce peut également être utilisé sur d'autres directives comme style-src.

Compatibilité navigateur

"Nonce" a été ajouté au CSP Level 2. La prise en charge existe depuis 2015 dans Chrome et Firefox, Safari 10+ et Edge 15+. Ils ne sont pas du tout pris en charge dans Internet Explorer. Conservez "unsafe-inline" pour la retro-compatibilité.

"strict-dynamic" est une fonctionnalité plus récente CSP Level 3. La prise en charge existe depuis 2017 dans Chrome et Firefox, Safari 15.4+ (2022) et Edge 79+ (2020). Conservez les domaines autorisés pour la retro-compatibilité.

Recommandation de rétrocompatibilité de PageSpeed Insights

script-src 'strict-dynamic' 'nonce-RaNdOm' https: 'unsafe-inline'

Intérêts

Injection

Il n'est plus possible d'injecter du code Javascript ou de charger un script externe sans avoir connaissance du nonce. La valeur du nonce de la directive CSP de l'en tête HTTP n'est jamais la même. De cette manière, un script injecté via un formulaire ou un paramètre dans l'URL ne sera jamais exécuté.

Dans l'exemple précédent, si je recharge la page en conservant la même valeur d'attribut nonce du script inline, le code ne sera pas exécuté :

script-src 'strict-dynamic' 'nonce-aL7K9IcwIOghPVh9m7Rsz8qu'; 
<script type="text/javascript" nonce="SC3tB3UYXQNKc4c81B1q37sr">
    myNotExecutedFunction();
</script>

Erreur: aL7K9IcwIOghPVh9m7Rsz8qu != SC3tB3UYXQNKc4c81B1q37sr

Scripts externes

La valeur strict-dynamic permet de charger des scripts supplémentaires, même externes, sans devoir systématiquement indiquer le domaine dans la liste des autorisations. Avec le nonce, le script est chargé par un script sûr et déjà fiable. Par exemple, pour un script hébergé sur un CDN externe :

Devient :

On ajoute simplement l'attribut "nonce" pour charger le script supplémentaire :

<script type="text/javascript"
        nonce="aL7K9IcwIOghPVh9m7Rsz8qu"
        src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js">
</script>
<script type="text/javascript" nonce="aL7K9IcwIOghPVh9m7Rsz8qu">
    let script = document.createElement('script');
    script.src = "https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js";
    document.body.appendChild(script);
</script>

Important : "strict-dynamic" est une fonctionnalité CSP Level 3, non supportée par les anciens navigateurs. Il est recommandé pour la retro-compatibilité de conserver la liste des domaines autorisés, ou plus simplement "https:".

Implémentation

Basique

Voici un exemple basique d'implémentation pour une simple page générée en PHP :

<?php
    $nonce = base64_encode(random_bytes(18));
    header("Content-Security-Policy: default-src 'self'; script-src 'strict-dynamic' 'nonce-" . $nonce . "' https:");
?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" lang="fr" xml:lang="fr">
  <head>
    <title>CSP Nonce</title>
    <script type="text/javascript" nonce="<?= $nonce ?>" src="..."></script>
  </head>
  <body>
    <script type="text/javascript" nonce="<?= $nonce ?>">
       myFunction();
    </script>
  </body>
</html>

Apache Server Side Includes (SSI)

Il est extrêmement courant sur les sites et applications web qu'une partie ou la totalité de la page soient placées dans un cache. Le nonce peut devenir alors plus délicat à mettre en place.

Les SSI Apache (Server Side Includes / Inclusions Côté Serveur) sont alors d'une grande aide. Elles permettent d'ajouter du contenu dynamique à des documents HTML préexistants. Concrètement, la page HTML est générée de n'importe quelle façon et dans n'importe quel langage, Apache se charge ensuite d'y remplacer des variables prédéfinies.

C'est également la seule méthode pour implémenter les nonces sur un site statique.

Vérifiez au préalable que les modules Apache mod_unique_id et mod_include sont bien activés :

sudo a2enmod unique_id
sudo a2enmod include

Dans le fichier vhost ou .htaccess, on utilise pour la valeur du "nonce" la variable UNIQUE_ID :

Options +Includes
AddOutputFilter INCLUDES .html .php

<IfModule mod_headers.c>
    Header set Content-Security-Policy "default-src 'self'; script-src 'strict-dynamic' 'nonce-%{UNIQUE_ID}e' https:"
</IfModule>

Le AddOutputFilter permet d'indiquer à Apache quels fichiers sont concernés.

Dans les templates HTML, il reste à ajouter l'attribut "nonce" pour tous les éléments script :

<script type="text/javascript" src="app.js" nonce="<!--#echo var='UNIQUE_ID' -->"></script>

<script type="text/javascript" nonce="<!--#echo var='UNIQUE_ID' -->">
    myFunction();
</script>

Pour plus d'informations :

mod_cspnonce

Pour faciliter l'implémentation, nous avons utilisé le mod_unique_id d'Apache. Ce n'est cependant pas la meilleure manière de générer un nonce.

Un module Apache tiers mod_cspnonce permet de générer des nonces cryptographiquement aléatoires, respectant les spécifications du CSP.

Télécharger le module compilé

Fichier compilé sur Ubuntu 22.04 : mod_cspnonce.so

Copier le fichier manuellement dans le dossier /usr/lib/apache2/modules/.

Compiler manuellement le module
git clone https://github.com/wyday/mod_cspnonce.git; cd mod_cspnonce
sudo apxs -ci mod_cspnonce.c

On active ensuite le module :

sudo echo "LoadModule cspnonce_module /usr/lib/apache2/modules/mod_cspnonce.so" > /etc/apache2/mods-available/cspnonce.load
sudo a2enmod cspnonce

La variable UNIQUE_ID utilisée précédemment peut maintenant être remplacée par la variable CSP_NONCE :

Options +Includes
AddOutputFilter INCLUDES .html .php

<IfModule mod_headers.c>
    Header set Content-Security-Policy "default-src 'self'; script-src 'strict-dynamic' 'nonce-%{CSP_NONCE}e' https:"
</IfModule>
<script type="text/javascript" src="app.js" nonce="<!--#echo var='CSP_NONCE' -->"></script>

<script type="text/javascript" nonce="<!--#echo var='CSP_NONCE' -->">
    myFunction();
</script>

Alwaysdata

Les hébergeurs (offres mutualisés) ne donnent pas toujours la main sur les modules Apache. Avec Alwaysdata, j'ai réussi à implémenter CSP Nonce avec le module mod_cspnonce sans aucune difficulté.

mod_cspnonce a été compilé en local sur une machine tournant sous Ubuntu 22.04 : mod_cspnonce.so

J'ai ensuite envoyé le fichier mod_cspnonce.so vers le serveur (ajoutez le dossier modules au préalable) :

scp /usr/lib/apache2/modules/mod_cspnonce.so username@ssh-username.alwaysdata.net:~/modules/mod_cspnonce.so
ssh username@ssh-username.alwaysdata.net
chmod 644 modules/mod_cspnonce.so

Sur l'interface, menu Web > Configuration on ajoute aux directives globales d'Apache :

LoadModule unique_id_module ${APACHE_ROOT}/modules/mod_unique_id.so
LoadModule include_module ${APACHE_ROOT}/modules/mod_include.so
LoadModule cspnonce_module /home/username/modules/mod_cspnonce.so

Directives globales d'Apache sur l'interface Alwaysdata

Dans les directives supplémentaires du virtual host de n'importe quel site, il est maintenance possible d'utiliser la variable CSP_NONCE :

Configuration avancée d'un site sur l'interface Alwaysdata

Une question ? Rejoignez-moi sur le Fédivers :
@magentix@magentix.space