Tous les articles

Pourquoi ne jamais utiliser unsafe-inline dans une CSP

CentralCSP Team ·

Dernière mise à jour:

Si votre politique de sécurité du contenu (CSP) contient 'unsafe-inline', elle est en grande partie décorative. La CSP est un en-tête de réponse HTTP qui indique au navigateur quels scripts et styles il a le droit d'exécuter. Son rôle le plus utile est de bloquer le JavaScript inline, ce sur quoi reposent la plupart des attaques de type cross-site scripting (XSS). Le mot-clé 'unsafe-inline' désactive cette protection.

En résumé : ne déployez pas 'unsafe-inline' dans une directive de script. Déplacez les scripts inline vers des fichiers externes, ou autorisez ceux que vous gardez avec un nonce ou un hash. La suite de cet article explique ce que fait vraiment ce mot-clé, pourquoi il neutralise la CSP, et comment vous en passer sans risque en commençant par le mode Report-Only.

Vous débutez avec la CSP ? Démarrer avec la politique de sécurité du contenu couvre d'abord les bases, ce qu'est la CSP, votre première politique et votre premier nonce.

Que fait 'unsafe-inline' ?

Par défaut, une CSP qui définit script-src bloque tout le JavaScript inline. Ajouter 'unsafe-inline' à cette directive le réautorise. Pour les scripts, le mot-clé autorise trois choses que le navigateur refuserait sinon d'exécuter :

  • Les blocs <script> inline (du code écrit directement dans la page).
  • Les attributs d'event handler inline comme onclick, onerror et onload.
  • Les URL javascript:.

Il existe un équivalent pour les styles. Dans style-src, 'unsafe-inline' autorise les blocs <style> inline et les attributs style="...". Le cas des styles est moins risqué que celui des scripts, mais les mêmes principes de migration s'appliquent.

Le mot-clé fait donc exactement ce qu'il annonce : il indique au navigateur que le code inline peut s'exécuter sans danger. Le problème, c'est que le navigateur n'a aucun moyen de distinguer votre code inline de celui d'un attaquant.

Pourquoi cela neutralise la protection XSS de la CSP

Une attaque XSS consiste à faire exécuter par le navigateur du balisage contrôlé par l'attaquant. Une injection classique prend la forme d'un bloc <script> inline, d'un event handler <img onerror=...>, ou d'une URL javascript: glissée dans une page via une entrée non assainie.

Avec 'unsafe-inline', le navigateur exécute n'importe quel script inline, quelle que soit sa provenance. Le script inline injecté s'exécute exactement comme votre script inline légitime, et la CSP ne dresse aucune barrière. La directive censée être votre seconde ligne de défense contre le XSS laisse désormais passer l'attaque.

Les nonces et les hash comblent cette faille car ils exigent quelque chose que l'attaquant ne peut pas fournir. Un nonce est une valeur aléatoire que le serveur génère à chaque réponse et que l'attaquant ne peut pas deviner. Un hash correspond à un bloc de code précis et connu, et du code injecté arbitraire n'y correspondra pas. Dans les deux cas, le navigateur exécute votre script inline et refuse celui qui est injecté.

La CSP relève de la défense en profondeur, ce n'est pas un substitut à l'assainissement des entrées. Vous continuez d'échapper et de valider les entrées. La CSP est la couche qui limite les dégâts quand quelque chose passe au travers, et 'unsafe-inline' retire cette couche. Pour aller plus loin et activer les Trusted Types pour éliminer le XSS DOM, vous pouvez aussi stopper l'injection au niveau du sink.

Les alternatives sûres

Vous avez trois façons de conserver le comportement inline sans 'unsafe-inline'. Choisissez au cas par cas ; la plupart des sites combinent les trois.

1. Externaliser le code

Déplacez les scripts et styles inline vers des fichiers servis depuis une origine déjà autorisée par votre politique. Une fois que le code vit dans /main.js au lieu d'un bloc <script>, il n'y a plus aucun mot-clé inline à ajouter.

Content-Security-Policy: script-src 'self'

C'est l'option la plus propre quand vous maîtrisez le code. En contrepartie, les petits extraits propres à une page et la configuration inline deviennent des fichiers supplémentaires ou sont injectés autrement.

2. Utiliser un nonce

Un nonce est une valeur cryptographiquement aléatoire que vous régénérez à chaque réponse HTTP. Vous la placez dans l'en-tête et la répétez sur chaque élément inline via l'attribut nonce. Le navigateur n'exécute que les scripts inline dont le nonce correspond à celui de l'en-tête. Pour les détails par framework, voyez comment mettre en place un nonce.

const nonce = crypto.randomUUID();
res.setHeader(
  "Content-Security-Policy",
  `script-src 'nonce-${nonce}'`
);
// <script nonce="${nonce}" src="/main.js"></script>
// <script nonce="${nonce}">console.log("hello!");</script>

Le nonce doit être imprévisible et unique pour chaque réponse. Le générer une seule fois et le réutiliser d'une requête à l'autre ruine tout l'intérêt, car un attaquant pourrait le lire sur une page et le réutiliser. On recommande couramment au moins 128 bits d'entropie.

3. Utiliser un hash

Un hash met en liste d'autorisation un bloc de code inline précis d'après son contenu. Vous calculez une empreinte SHA-256, 384 ou 512 du contenu inline, vous l'encodez en base64, et vous l'ajoutez comme source sous la forme 'sha256-...'.

Content-Security-Policy: script-src 'sha256-...'

Le hash est calculé sur le contenu inline exact, le texte entre les balises, pas les balises elles-mêmes. Les espaces et la casse font partie du contenu, donc un seul espace modifié invalide le hash. Collez l'extrait dans notre générateur de hash : il renvoie la valeur de source 'sha256-...' à coller directement dans votre politique, ou suivez la démonstration sur comment générer un hash CSP.

Les hash conviennent aux blocs inline statiques qui changent rarement, comme un bundle inline généré à la compilation. Les nonces conviennent aux pages rendues côté serveur, où vous maîtrisez la réponse.

Le piège des event handlers inline

Les nonces et les hash ne couvrent pas les event handlers inline comme onclick. Autoriser un handler par hash nécessite le mot-clé distinct 'unsafe-hashes', qui assouplit la politique. La meilleure solution est de supprimer complètement les event handlers inline et de les brancher avec addEventListener dans votre script externe ou porteur d'un nonce.

<!-- Avant : event handler inline, nécessite 'unsafe-inline' ou 'unsafe-hashes' -->
<button onclick="saveForm()">Save</button>
// Après : plus d'event handler inline, fonctionne avec une politique stricte par nonce ou hash
// balisage : <button id="save">Save</button>
document.getElementById("save").addEventListener("click", saveForm);

Comment les directives s'articulent

Quand on creuse, la CSP répartit la gestion de l'inline entre des directives plus spécifiques, et script-src-elem vs script-src-attr compare les deux qui comptent pour le code inline :

  • script-src régit les scripts inline lorsque les directives plus spécifiques sont absentes.
  • script-src-elem régit les blocs <script> inline.
  • script-src-attr régit les event handlers inline. Ses seules valeurs utiles sont 'unsafe-hashes', 'unsafe-inline' et 'report-sample'.

Les styles suivent le même schéma avec style-src, style-src-elem et style-src-attr. Les directives -elem et -attr se rabattent sur script-src ou style-src, qui se rabattent sur default-src.

'unsafe-inline' a un mot-clé cousin qui affaiblit aussi la politique et qu'il vaut la peine de connaître, 'unsafe-eval', qui réactive l'exécution de chaîne en code de la même façon que celui-ci réactive le script inline.

Une règle fait ici tout le travail. Si une directive contient une source de type nonce ou hash, le navigateur ignore 'unsafe-inline' dans cette même directive, comme le définit l'algorithme allow-all-inline du W3C, reproduit dans le AllowAllInline() de Chromium. Donc si 'unsafe-inline' se retrouve à côté d'un nonce ou d'un hash, le nonce ou le hash vous protège quand même. N'en faites pas une fonctionnalité : retirez simplement 'unsafe-inline'.

Pour les directives de script, 'strict-dynamic' va plus loin. Il indique au navigateur d'ignorer entièrement les listes d'hôtes autorisés, les sources de schéma, 'self' et 'unsafe-inline', et de ne faire confiance aux scripts que via un nonce ou un hash sur un script racine (et les scripts que ce script racine charge). Il n'a aucun effet sur style-src, et c'est la façon moderne d'autoriser les scripts sans listes d'hôtes fragiles :

Content-Security-Policy: script-src 'nonce-r4nd0m' 'strict-dynamic'

'unsafe-inline', script-src, style-src, les nonces, les hash et 'strict-dynamic' sont tous largement pris en charge par les navigateurs actuels.

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

Un chemin de migration sûr pour abandonner unsafe-inline

Retirer 'unsafe-inline' d'un site en production cassera tout ce qui en dépend, alors avancez par étapes et surveillez les rapports avant de passer en mode bloquant. C'est la même approche Report-Only d'abord que celle décrite dans comment construire une CSP solide, étape par étape.

  1. Auditez votre usage de l'inline. Repérez chaque <script> inline, chaque event handler inline et chaque URL javascript:. Notre scanner CSP et notre évaluateur CSP signalent où une politique repose encore sur 'unsafe-inline'.
  2. Convertissez chaque cas. Externalisez ce que vous pouvez, ajoutez un nonce aux scripts inline rendus côté serveur, hachez les scripts statiques, et remplacez les event handlers inline par addEventListener.
  3. Testez en Report-Only. Diffusez la nouvelle politique sur l'en-tête Content-Security-Policy-Report-Only. Le navigateur ne bloque rien, il envoie seulement un rapport de violation pour ce que la politique aurait bloqué. Collectez ces rapports et corrigez les manques.
  4. Passez en mode bloquant. Quand le flux Report-Only est calme, déplacez la politique vers l'en-tête bloquant Content-Security-Policy et retirez 'unsafe-inline' de vos directives de script.

Le mode Report-Only est l'étape qui rend tout cela sûr. Vous voyez chaque violation qu'une politique plus stricte provoquerait, sur du trafic réel, avant qu'un utilisateur ne tombe sur une page cassée. Pour inspecter une seule page sur votre propre machine, vous pouvez aussi déboguer les violations CSP dans DevTools.

CentralCSP collecte ces rapports de violation Report-Only pour vous, les regroupe, et montre les scripts inline qui s'exécutent sur chaque page grâce au signalement par hash de la CSP, pour que vous sachiez exactement quoi externaliser, signer d'un nonce ou hacher avant de passer en mode bloquant. C'est le cœur de notre suite CSP. Vous pouvez démarrer un essai gratuit et pointer un en-tête Report-Only vers la plateforme pour suivre la migration en temps réel.

Pour la référence complète des directives et des mots-clés, voyez la référence de la politique CSP. Pour en savoir plus sur le mot-clé qui laisse les scripts de confiance charger d'autres scripts, voyez le guide 'strict-dynamic'. Pour un exemple concret de nonce plus 'strict-dynamic' avec un vrai tiers, voyez faire tourner Google Analytics et Tag Manager sous une CSP stricte.

Foire aux questions

unsafe-inline est-il sûr ?

Non. Il réautorise précisément l'exécution de scripts inline que la CSP est censée bloquer, donc un script inline injecté s'exécute comme n'importe quel autre. Ne le gardez que comme repli dans une directive qui porte aussi un nonce ou un hash, là où les navigateurs modernes l'ignorent.

Quelle est la différence entre un nonce et un hash ?

Un nonce est une valeur aléatoire fraîche que vous placez dans l'en-tête et sur chaque élément inline à chaque réponse, idéale pour les pages rendues côté serveur. Un hash met en liste d'autorisation un bloc de code inline exact et figé, idéal pour les extraits statiques.

'strict-dynamic' remplace-t-il 'unsafe-inline' ?

Pour les scripts, oui. Avec 'strict-dynamic', le navigateur ignore 'unsafe-inline', les listes d'hôtes et 'self', et ne fait confiance aux scripts que via un nonce ou un hash. Cela ne s'applique pas aux styles.

unsafe-inline est-il parfois sans danger ?

Seulement lorsque la même directive contient aussi un nonce ou un hash, car le navigateur ignore alors 'unsafe-inline'. Même dans ce cas, retirez-le pour que la politique dise exactement ce qu'elle fait.

À retenir

'unsafe-inline' n'est pas une option de confort, c'est un interrupteur qui éteint la protection pour laquelle vous avez activé la CSP. Retirez-le de vos directives de script. S'il se retrouve à côté d'un nonce ou d'un hash, le navigateur l'ignore de toute façon, vous ne perdez donc rien à le supprimer. Externalisez, signez d'un nonce ou hachez votre code inline, et migrez via le mode Report-Only pour ne jamais casser la page en voulant la sécuriser.

Pour aller plus loin : le guide CSP de MDN et la spécification W3C CSP niveau 3.