Limiter les scripts intersites (XSS) avec une CSP (Content Security Policy) stricte.

Lukas Weichselbaum
Lukas Weichselbaum

Navigateurs pris en charge

  • Chrome: 52.
  • Edge: 79.
  • Firefox: 52.
  • Safari: 15.4.

Source

Le script intersites (XSS), qui permet d'injecter des scripts malveillants dans une application Web, est l'une des plus grandes failles de sécurité Web depuis plus de 10 ans.

La Content Security Policy (CSP) est une couche de sécurité supplémentaire qui permet de limiter les attaques XSS. Pour configurer un CSP, ajoutez l'en-tête HTTP Content-Security-Policy à une page Web et définissez des valeurs qui contrôlent les ressources que l'user-agent peut charger pour cette page.

Cette page explique comment utiliser un CSP basé sur des nonces ou des hachages pour atténuer les attaques XSS, au lieu des CSP basés sur une liste d'autorisation d'hôte couramment utilisés, qui laissent souvent la page exposée aux attaques XSS, car ils peuvent être contournés dans la plupart des configurations.

Terme clé: Un nonce est un nombre aléatoire utilisé une seule fois que vous pouvez utiliser pour marquer une balise <script> comme fiable.

Terme clé: une fonction de hachage est une fonction mathématique qui convertit une valeur d'entrée en valeur numérique compressée appelée hachage. Vous pouvez utiliser un hachage (par exemple, SHA-256) pour marquer une balise <script> intégrée comme fiable.

Une règle CSP basée sur des nonces ou des hachages est souvent appelée CSP strict. Lorsqu'une application utilise un CSP strict, les pirates informatiques qui détectent des failles d'injection HTML ne peuvent généralement pas les utiliser pour forcer le navigateur à exécuter des scripts malveillants dans un document vulnérable. En effet, le CSP strict n'autorise que les scripts hachés ou les scripts avec la valeur nonce correcte générée sur le serveur. Les pirates informatiques ne peuvent donc pas exécuter le script sans connaître le nonce correct pour une réponse donnée.

Pourquoi utiliser un CSP strict ?

Si votre site dispose déjà d'un CSP semblable à script-src www.googleapis.com, il n'est probablement pas efficace contre les attaques intersites. Ce type de CSP est appelé CSP de liste d'autorisation. Ils nécessitent une grande personnalisation et peuvent être contournés par des pirates informatiques.

Les CSP stricts basés sur des nonces ou des hachages cryptographiques évitent ces écueils.

Structure CSP stricte

Une règle de base de sécurité du contenu stricte utilise l'un des en-têtes de réponse HTTP suivants:

CSP stricte basée sur un nonce

Content-Security-Policy:
  script-src 'nonce-{RANDOM}' 'strict-dynamic';
  object-src 'none';
  base-uri 'none';
Fonctionnement d'un CSP strict basé sur un nonce.

CSP stricte basée sur les hachages

Content-Security-Policy:
  script-src 'sha256-{HASHED_INLINE_SCRIPT}' 'strict-dynamic';
  object-src 'none';
  base-uri 'none';

Les propriétés suivantes rendent un CSP comme celui-ci "strict" et donc sécurisé:

  • Il utilise des nonces 'nonce-{RANDOM}' ou des hachages 'sha256-{HASHED_INLINE_SCRIPT}' pour indiquer les balises <script> que le développeur du site peut exécuter dans le navigateur de l'utilisateur.
  • Il définit 'strict-dynamic' pour réduire les efforts de déploiement d'un CSP basé sur un nonce ou un hachage en autorisant automatiquement l'exécution des scripts créés par un script approuvé. Cela permet également de débloquer l'utilisation de la plupart des widgets et bibliothèques JavaScript tiers.
  • Il n'est pas basé sur des listes d'autorisation d'URL. Il n'est donc pas sujet aux contournements courants des CSP.
  • Il bloque les scripts intégrés non approuvés, tels que les gestionnaires d'événements intégrés ou les URI javascript:.
  • Il limite object-src à la désactivation des plug-ins dangereux tels que Flash.
  • Il limite base-uri pour bloquer l'injection de balises <base>. Cela empêche les pirates informatiques de modifier les emplacements des scripts chargés à partir d'URL relatives.

Adopter une CSP stricte

Pour adopter un CSP strict, vous devez:

  1. Déterminez si votre application doit définir un CSP basé sur un nonce ou un hachage.
  2. Copiez le CSP de la section Structure stricte du CSP et définissez-le comme en-tête de réponse dans votre application.
  3. Refactorisez les modèles HTML et le code côté client pour supprimer les modèles incompatibles avec le CSP.
  4. Déployez votre CSP.

Vous pouvez utiliser l'audit Bonnes pratiques de Lighthouse (version 7.3.0 et ultérieure avec l'indicateur --preset=experimental) tout au long de ce processus pour vérifier si votre site dispose d'un CSP et s'il est suffisamment strict pour être efficace contre les XSS.

Avertissement du rapport Lighthouse indiquant qu&#39;aucun CSP n&#39;est détecté en mode de mise en conformité.
Si votre site ne comporte pas de CSP, Lighthouse affiche cet avertissement.

Étape 1: Déterminez si vous avez besoin d'un CSP basé sur un nonce ou un hachage

Voici comment fonctionnent les deux types de CSP stricts:

CSP basé sur un nonce

Avec un CSP basé sur un nonce, vous générez un nombre aléatoire au moment de l'exécution, l'incluez dans votre CSP et l'associez à chaque balise de script de votre page. Un pirate informatique ne peut pas inclure ni exécuter de script malveillant sur votre page, car il devrait deviner le bon nombre aléatoire pour ce script. Cela ne fonctionne que si le nombre n'est pas devinable et est généré à chaque exécution pour chaque réponse.

Utilisez un CSP basé sur un nonce pour les pages HTML affichées sur le serveur. Pour ces pages, vous pouvez créer un nouveau nombre aléatoire pour chaque réponse.

CSP basée sur les hachages

Pour un CSP basé sur un hachage, le hachage de chaque balise de script intégrée est ajouté au CSP. Chaque script a un hachage différent. Un pirate informatique ne peut pas inclure ni exécuter de script malveillant sur votre page, car le hachage de ce script doit se trouver dans votre CSP pour qu'il s'exécute.

Utilisez un CSP basé sur un hachage pour les pages HTML diffusées de manière statique ou les pages qui doivent être mises en cache. Par exemple, vous pouvez utiliser un CSP basé sur un hachage pour les applications Web monopages créées avec des frameworks tels qu'Angular, React ou d'autres, qui sont diffusées de manière statique sans rendu côté serveur.

Étape 2: Définissez un CSP strict et préparez vos scripts

Vous avez plusieurs options pour définir un CSP:

  • Mode "Rapport uniquement" (Content-Security-Policy-Report-Only) ou mode d'application (Content-Security-Policy). En mode "Rapport uniquement", le CSP ne bloque pas encore les ressources. Par conséquent, rien ne s'arrête sur votre site, mais vous pouvez voir des erreurs et obtenir des rapports sur tout ce qui aurait été bloqué. Localement, lorsque vous définissez votre CSP, cela n'a pas vraiment d'importance, car les deux modes affichent les erreurs dans la console du navigateur. Le mode d'application peut vous aider à trouver les ressources bloquées par votre projet de CSP, car le blocage d'une ressource peut donner l'impression que votre page est défectueuse. Le mode "Rapports uniquement" est le plus utile plus tard dans le processus (voir Étape 5).
  • En-tête ou balise HTML <meta>. Pour le développement local, une balise <meta> peut être plus pratique pour ajuster votre CSP et voir rapidement son impact sur votre site. Toutefois :
    • Plus tard, lorsque vous déploierez votre CSP en production, nous vous recommandons de le définir en tant qu'en-tête HTTP.
    • Si vous souhaitez définir votre CSP en mode "Rapport uniquement", vous devez le définir en tant qu'en-tête, car les métabalises CSP ne sont pas compatibles avec ce mode.

Option A: CSP basé sur un nonce

Définissez l'en-tête de réponse HTTP Content-Security-Policy suivant dans votre application:

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

Générer un nonce pour le CSP

Un nonce est un nombre aléatoire utilisé une seule fois par chargement de page. Un CSP basé sur un nonce ne peut atténuer les attaques XSS que si les pirates informatiques ne peuvent pas deviner la valeur du nonce. Un nonce CSP doit être:

  • Une valeur aléatoire cryptographiquement sécurisée (idéalement 128 bits ou plus)
  • Généré à nouveau pour chaque réponse
  • Encodage en base64

Voici quelques exemples d'ajout d'un nonce CSP dans des frameworks côté serveur:

const app = express();

app.get('/', function(request, response) {
  // Generate a new random nonce value for every response.
  const nonce = crypto.randomBytes(16).toString("base64");

  // Set the strict nonce-based CSP response header
  const csp = `script-src 'nonce-${nonce}' 'strict-dynamic'; object-src 'none'; base-uri 'none';`;
  response.set("Content-Security-Policy", csp);

  // Every <script> tag in your application should set the `nonce` attribute to this value.
  response.render(template, { nonce: nonce });
});

Ajouter un attribut nonce aux éléments <script>

Avec un CSP basé sur un nonce, chaque élément <script> doit avoir un attribut nonce correspondant à la valeur de nonce aléatoire spécifiée dans l'en-tête du CSP. Tous les scripts peuvent avoir le même nonce. La première étape consiste à ajouter ces attributs à tous les scripts afin que le CSP les autorise.

Option B: En-tête de réponse CSP basé sur un hachage

Définissez l'en-tête de réponse HTTP Content-Security-Policy suivant dans votre application:

Content-Security-Policy:
  script-src 'sha256-{HASHED_INLINE_SCRIPT}' 'strict-dynamic';
  object-src 'none';
  base-uri 'none';

Pour plusieurs scripts intégrés, la syntaxe est la suivante : 'sha256-{HASHED_INLINE_SCRIPT_1}' 'sha256-{HASHED_INLINE_SCRIPT_2}'.

Charger des scripts sources de manière dynamique

Vous pouvez charger des scripts tiers de manière dynamique à l'aide d'un script intégré.

Exemple d'intégration de vos scripts.
Autorisé par le CSP
<script>
  var scripts = [ 'https://example.org/foo.js', 'https://example.org/bar.js'];

  scripts.forEach(function(scriptUrl) {
    var s = document.createElement('script');
    s.src = scriptUrl;
    s.async = false; // to preserve execution order
    document.head.appendChild(s);
  });
</script>
Pour exécuter ce script, vous devez calculer le hachage du script intégré et l'ajouter à l'en-tête de réponse du CSP, en remplaçant l'espace réservé {HASHED_INLINE_SCRIPT}. Pour réduire le nombre de hachages, vous pouvez fusionner tous les scripts intégrés en un seul script. Pour voir une démonstration, consultez cet exemple et son code.
Bloqué par le CSP
<script src="https://example.org/foo.js"></script>
<script src="https://example.org/bar.js"></script>
Le CSP bloque ces scripts, car ils n'ont pas été ajoutés de manière dynamique et ne comportent aucun attribut integrity correspondant à une source autorisée.

Considérations concernant le chargement de script

L'exemple de script intégré ajoute s.async = false pour s'assurer que foo s'exécute avant bar, même si bar se charge en premier. Dans cet extrait, s.async = false ne bloque pas l'analyseur pendant le chargement des scripts, car ils sont ajoutés de manière dynamique. L'analyseur ne s'arrête que pendant l'exécution des scripts, comme il le ferait pour les scripts async. Toutefois, avec cet extrait, gardez à l'esprit les points suivants:

  • L'un ou les deux scripts peuvent s'exécuter avant la fin du téléchargement du document. Si vous souhaitez que le document soit prêt au moment de l'exécution des scripts, attendez l'événement DOMContentLoaded avant d'ajouter les scripts. Si cela entraîne un problème de performances, car les scripts ne commencent pas à se télécharger suffisamment tôt, utilisez des balises de préchargement plus tôt sur la page.
  • defer = true ne fait rien. Si vous avez besoin de ce comportement, exécutez le script manuellement lorsque cela est nécessaire.

Étape 3: Refactorisez les modèles HTML et le code côté client

Les gestionnaires d'événements intégrés (tels que onclick="…", onerror="…") et les URI JavaScript (<a href="javascript:…">) peuvent être utilisés pour exécuter des scripts. Cela signifie qu'un pirate informatique qui trouve un bug XSS peut injecter ce type de code HTML et exécuter du code JavaScript malveillant. Un CSP basé sur un nonce ou un hachage interdit l'utilisation de ce type de balisage. Si votre site utilise l'un de ces modèles, vous devrez le refactoriser en solutions plus sûres.

Si vous avez activé le CSP à l'étape précédente, vous pourrez voir les cas de non-respect du CSP dans la console chaque fois que le CSP bloque un modèle incompatible.

Rapports de non-respect du CSP dans la console de développement Chrome
Erreurs de console pour le code bloqué.

Dans la plupart des cas, la solution est simple:

Refactoriser les gestionnaires d'événements intégrés

Autorisé par le CSP
<span id="things">A thing.</span>
<script nonce="${nonce}">
  document.getElementById('things').addEventListener('click', doThings);
</script>
Le CSP autorise les gestionnaires d'événements enregistrés à l'aide de JavaScript.
Bloqué par le CSP
<span onclick="doThings();">A thing.</span>
Le CSP bloque les gestionnaires d'événements intégrés.

Refactoriser les URI javascript:

Autorisé par le CSP
<a id="foo">foo</a>
<script nonce="${nonce}">
  document.getElementById('foo').addEventListener('click', linkClicked);
</script>
Le CSP autorise les gestionnaires d'événements enregistrés à l'aide de JavaScript.
Bloqué par le CSP
<a href="javascript:linkClicked()">foo</a>
Le CSP bloque les URI javascript:.

Supprimer eval() de votre code JavaScript

Si votre application utilise eval() pour convertir les sérialisations de chaînes JSON en objets JS, vous devez refactoriser ces instances en JSON.parse(), ce qui est également plus rapide.

Si vous ne pouvez pas supprimer toutes les utilisations de eval(), vous pouvez toujours définir un CSP strict basé sur un nonce, mais vous devez utiliser le mot clé CSP 'unsafe-eval', ce qui rend votre stratégie légèrement moins sécurisée.

Vous trouverez d'autres exemples de ce type de refactoring dans cet atelier de programmation CSP strict:

Étape 4 (facultatif): Ajoutez des solutions de remplacement pour prendre en charge les anciennes versions de navigateur

Navigateurs pris en charge

  • Chrome: 52.
  • Edge: 79.
  • Firefox: 52.
  • Safari: 15.4.

Source

Si vous devez prendre en charge d'anciennes versions de navigateur:

  • L'utilisation de strict-dynamic nécessite d'ajouter https: comme solution de remplacement pour les versions antérieures de Safari. Voici ce que vous pouvez faire :
    • Tous les navigateurs compatibles avec strict-dynamic ignorent le remplacement https:. Par conséquent, cela ne réduit pas l'efficacité de la règle.
    • Dans les anciens navigateurs, les scripts provenant d'une source externe ne peuvent se charger que s'ils proviennent d'une origine HTTPS. Cette approche est moins sécurisée qu'une CSP stricte, mais elle empêche tout de même certaines causes courantes de XSS, comme les injections d'URI javascript:.
  • Pour assurer la compatibilité avec les versions de navigateurs très anciennes (plus de quatre ans), vous pouvez ajouter unsafe-inline comme solution de secours. Tous les navigateurs récents ignorent unsafe-inline si un nonce ou un hachage CSP est présent.
Content-Security-Policy:
  script-src 'nonce-{random}' 'strict-dynamic' https: 'unsafe-inline';
  object-src 'none';
  base-uri 'none';

Étape 5: Déployez votre CSP

Après avoir vérifié que votre CSP ne bloque aucun script légitime dans votre environnement de développement local, vous pouvez le déployer en préproduction, puis dans votre environnement de production:

  1. (Facultatif) Déployez votre CSP en mode "Rapports uniquement" à l'aide de l'en-tête Content-Security-Policy-Report-Only. Le mode "Rapport uniquement" est utile pour tester un changement potentiellement bloquant, comme un nouveau CSP en production, avant de commencer à appliquer les restrictions du CSP. En mode "Rapport uniquement", votre CSP n'affecte pas le comportement de votre application, mais le navigateur génère toujours des erreurs de console et des rapports de non-respect lorsqu'il rencontre des modèles incompatibles avec votre CSP. Vous pouvez ainsi voir ce qui aurait été cassé pour vos utilisateurs finaux. Pour en savoir plus, consultez la section API Reporting.
  2. Lorsque vous êtes sûr que votre CSP ne cassera pas votre site pour vos utilisateurs finaux, déployez-le à l'aide de l'en-tête de réponse Content-Security-Policy. Nous vous recommandons de définir votre CSP à l'aide d'un en-tête HTTP côté serveur, car il est plus sécurisé qu'une balise <meta>. Une fois cette étape terminée, votre CSP commence à protéger votre application contre les attaques XSS.

Limites

Une CSP stricte fournit généralement une couche de sécurité supplémentaire efficace qui permet de limiter les attaques XSS. Dans la plupart des cas, la CSP réduit considérablement la surface d'attaque en rejetant les modèles dangereux tels que les URI javascript:. Toutefois, en fonction du type de CSP que vous utilisez (nonces, hachages, avec ou sans 'strict-dynamic'), il existe des cas où le CSP ne protège pas aussi bien votre application:

  • Si vous noncez un script, mais qu'il y a une injection directement dans le corps ou le paramètre src de cet élément <script>.
  • Si des injections sont effectuées dans les emplacements des scripts créés dynamiquement (document.createElement('script')), y compris dans les fonctions de bibliothèque qui créent des nœuds DOM script en fonction des valeurs de leurs arguments. Cela inclut certaines API courantes telles que .html() de jQuery, ainsi que .get() et .post() dans jQuery < 3.0.
  • Si des injections de modèles sont présentes dans d'anciennes applications AngularJS. Un pirate informatique qui peut injecter du code dans un modèle AngularJS peut l'utiliser pour exécuter du code JavaScript arbitraire.
  • Si la règle contient 'unsafe-eval', des injections dans eval(), setTimeout() et quelques autres API rarement utilisées.

Les développeurs et les ingénieurs en sécurité doivent accorder une attention particulière à ces tendances lors des examens de code et des audits de sécurité. Pour en savoir plus sur ces cas, consultez Content Security Policy: A Successful Mess Between Hardening and Mitigation.

Documentation complémentaire