Content Security Policy (CSP) : contrôler les ressources chargées par votre site
Vous développez une application web, vous avez mis en place HTTPS, peut-être même HSTS. Votre serveur est à jour, vos dépendances aussi. Et pourtant, une simple faille XSS peut suffire à compromettre vos utilisateurs. Un script malveillant injecté dans une page, et c’est le vol de session, le redirect vers un site de phishing ou l’exfiltration de données sensibles.
Les failles Cross-Site Scripting (XSS) restent parmi les vulnérabilités les plus fréquentes sur le web. Elles exploitent un principe simple : le navigateur fait confiance à tout ce que le serveur lui envoie. Si un attaquant parvient à glisser du code dans votre page, le navigateur l’exécute sans poser de questions.
C’est précisément là qu’intervient la Content Security Policy. CSP permet de dire au navigateur : “voici les sources autorisées pour charger des scripts, des styles, des images, des fonts… tout le reste, tu bloques.” Historiquement pensée pour contrer les XSS, la CSP va en réalité bien au-delà : contrôle du chargement des iframes, protection contre le clickjacking, détection du mixed content HTTP/HTTPS, et même surveillance des violations via un système de reporting.
Nous allons voir ensemble comment fonctionne CSP, quelles directives utiliser, comment la déployer progressivement sans casser votre site, et surtout quelles erreurs éviter.
Sommaire
- Pourquoi vos pages web sont vulnérables
- Content Security Policy : le principe
- Les directives CSP essentielles
- Tester sans casser : le mode Report-Only
- Les erreurs qui rendent votre CSP inutile
- Mettre en place CSP concrètement
Pourquoi vos pages web sont vulnérables
Par défaut, un navigateur charge et exécute tout ce qu’une page HTML lui demande. Un <script> inline ? Exécuté. Une image chargée depuis un domaine inconnu ? Affichée. Un iframe pointant vers un site tiers ? Rendu. Le navigateur ne fait aucune distinction entre le contenu légitime et le contenu injecté par un attaquant.
Les failles XSS exploitent exactement ce comportement. On distingue généralement trois variantes :
- XSS Stored (persistant) : le code malveillant est stocké côté serveur (en base de données par exemple) et servi à chaque visiteur. Un commentaire piégé sur un forum en est l’exemple classique.
- XSS Reflected : le code est injecté via l’URL ou un paramètre de requête, puis renvoyé dans la réponse du serveur sans être stocké. Typiquement via un lien piégé envoyé par email.
- XSS DOM-based : l’injection se produit côté client, directement dans le DOM, sans passer par le serveur. Le JavaScript de la page manipule des données non fiables (fragment d’URL,
document.referrer…) sans les assainir.
Dans les trois cas, le résultat est le même : du code JavaScript arbitraire s’exécute dans le contexte de votre page, avec accès aux cookies, au DOM, et potentiellement aux tokens d’authentification de vos utilisateurs.
La validation et l’échappement des entrées utilisateur restent évidemment la première ligne de défense. Mais une seule erreur, un seul oubli dans un template, et la porte est ouverte. CSP agit comme un filet de sécurité supplémentaire : même si du code malveillant parvient à s’injecter dans votre HTML, le navigateur refusera de l’exécuter si la politique ne l’autorise pas.
Content Security Policy : le principe
CSP fonctionne de manière très simple. Votre serveur envoie un en-tête HTTP Content-Security-Policy avec la réponse. Cet en-tête contient une liste de directives qui indiquent au navigateur quelles sources sont autorisées pour chaque type de ressource.
Concrètement, quand le navigateur reçoit cet en-tête :
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.exemple.com
Il comprend : “pour cette page, charge les ressources uniquement depuis l’origine du site ('self'), et autorise les scripts depuis le site lui-même et depuis https://cdn.exemple.com. Tout le reste est bloqué.”
Si un attaquant injecte un <script src="https://evil.com/malware.js"> dans votre page, le navigateur le bloquera car evil.com n’est pas dans la liste des sources autorisées pour les scripts. Idem pour un <script>alert('xss')</script> inline : par défaut, CSP interdit l’exécution de scripts inline.
Il existe deux façons de déclarer une CSP :
- Via l’en-tête HTTP (recommandé) : le serveur ajoute
Content-Security-Policydans les headers de la réponse. - Via une balise
<meta>: directement dans le HTML avec<meta http-equiv="Content-Security-Policy" content="...">.
Personnellement je vous conseille l’en-tête HTTP. La balise <meta> a des limitations : elle ne supporte pas certaines directives comme frame-ancestors ou report-uri, et elle peut être contournée si l’attaquant parvient à injecter du HTML avant la balise <meta>.
Les directives CSP essentielles
CSP propose un ensemble de directives, chacune contrôlant un type de ressource spécifique. Nous n’allons pas toutes les lister ici, mais voici celles que vous utiliserez le plus souvent :
| Directive | Contrôle | Exemple |
|---|---|---|
default-src |
Politique par défaut pour toutes les ressources non spécifiées | default-src 'self' |
script-src |
Sources autorisées pour les scripts JavaScript | script-src 'self' https://cdn.exemple.com |
style-src |
Sources autorisées pour les feuilles de style CSS | style-src 'self' 'unsafe-inline' |
img-src |
Sources autorisées pour les images | img-src 'self' data: https: |
connect-src |
Cibles autorisées pour les requêtes XHR, Fetch, WebSocket | connect-src 'self' https://api.exemple.com |
font-src |
Sources autorisées pour les polices de caractères | font-src 'self' https://fonts.gstatic.com |
frame-ancestors |
Qui peut intégrer votre page dans une iframe | frame-ancestors 'none' |
object-src |
Sources pour les plugins (Flash, Java…) | object-src 'none' |
base-uri |
Valeurs autorisées pour la balise <base> |
base-uri 'self' |
La directive default-src est fondamentale. Elle sert de politique de repli : si vous ne définissez pas script-src, c’est default-src qui s’applique aux scripts. Personnellement je vous conseille de toujours commencer par default-src 'self' puis d’ouvrir au cas par cas.
Les valeurs les plus courantes que vous pouvez attribuer à ces directives :
'self': autorise les ressources provenant de la même origine (même schéma, domaine et port).'none': bloque tout. Utile pourobject-srcpar exemple, puisque les plugins sont rarement nécessaires aujourd’hui.https://domaine.com: autorise un domaine spécifique.data:: autorise les ressources en data URI (images en base64 par exemple).'unsafe-inline': autorise les scripts ou styles inline. À éviter autant que possible (nous y reviendrons).
Un exemple de CSP raisonnablement stricte pour un site classique :
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' https://fonts.gstatic.com; object-src 'none'; frame-ancestors 'self'; base-uri 'self'
Tester sans casser : le mode Report-Only
Déployer une CSP sur un site existant sans préparation, c’est la garantie de casser des fonctionnalités. Un script tiers oublié, un style inline dans un vieux template, une image chargée depuis un CDN non listé, et voilà des éléments de votre page qui disparaissent silencieusement.
C’est pour ça que l’en-tête Content-Security-Policy-Report-Only existe. Il fonctionne exactement comme Content-Security-Policy, mais au lieu de bloquer les ressources non conformes, il se contente de signaler les violations. Votre site continue de fonctionner normalement, et vous collectez les informations nécessaires pour affiner votre politique.
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self'; report-uri /csp-report
Avec cette configuration, le navigateur envoie un rapport JSON à /csp-report à chaque fois qu’une ressource viole la politique. Voici à quoi ressemble un rapport typique :
{
"csp-report": {
"document-uri": "https://monsite.com/page",
"referrer": "",
"violated-directive": "script-src 'self'",
"effective-directive": "script-src",
"original-policy": "default-src 'self'; script-src 'self'; report-uri /csp-report",
"blocked-uri": "https://cdn.tracker-inconnu.com/analytics.js",
"status-code": 200
}
}
Ce rapport nous dit clairement que la page /page a tenté de charger un script depuis cdn.tracker-inconnu.com, ce qui viole la directive script-src 'self'. À partir de là, deux choix : soit vous ajoutez ce domaine à votre politique parce que c’est un script légitime (un outil analytics par exemple), soit vous confirmez que c’est bien un chargement non désiré et vous gardez la restriction.
Collecter les rapports
Pour réceptionner ces rapports, il vous faut un endpoint capable de recevoir du JSON en POST. Un exemple minimaliste en Node.js :
app.post('/csp-report', express.json({ type: 'application/csp-report' }), (req, res) => {
console.log('Violation CSP :', req.body['csp-report']);
res.status(204).end();
});
En pratique, sur un site avec du trafic, vous allez recevoir beaucoup de rapports (les extensions navigateur déclenchent souvent des violations). Personnellement je vous conseille de stocker les rapports en base et de les agréger par blocked-uri et violated-directive pour repérer rapidement les patterns significatifs et filtrer le bruit.
L’approche progressive
- Déployez votre CSP en mode Report-Only.
- Analysez les rapports pendant quelques jours ou semaines.
- Ajustez les directives en fonction des violations légitimes.
- Une fois la politique stabilisée, passez en mode actif avec
Content-Security-Policy.
Vous pouvez d’ailleurs utiliser les deux en-têtes simultanément : garder votre CSP active en production et tester une politique plus stricte en Report-Only. C’est l’approche incrémentale idéale pour renforcer progressivement votre sécurité sans jamais prendre le risque de casser des fonctionnalités.
Les erreurs qui rendent votre CSP inutile
Avoir un en-tête CSP ne signifie pas que votre site est protégé. Une CSP mal configurée donne un faux sentiment de sécurité. Voici les erreurs les plus fréquentes.
Utiliser unsafe-inline pour les scripts
C’est l’erreur la plus courante et la plus grave. script-src 'unsafe-inline' autorise l’exécution de n’importe quel script inline dans votre page. Or, c’est exactement le vecteur d’attaque principal des XSS. Avec cette directive, votre CSP ne protège plus contre grand-chose.
/* À ne surtout pas faire */
Content-Security-Policy: script-src 'self' 'unsafe-inline'
Si vous avez besoin d’exécuter des scripts inline (et c’est souvent le cas avec des frameworks ou des outils analytics), préférez l’approche par nonce ou par hash plutôt que d’ouvrir la porte avec unsafe-inline.
Le nonce est une valeur aléatoire générée à chaque requête par le serveur. Elle est ajoutée à la fois dans l’attribut nonce du script et dans la directive CSP. Le navigateur n’exécute que les scripts dont le nonce correspond :
<script nonce="abc123random">
// Ce script sera autorisé car le nonce correspond
</script>
Content-Security-Policy: script-src 'self' 'nonce-abc123random'
Le nonce doit impérativement être unique à chaque réponse HTTP. Si vous réutilisez le même nonce, un attaquant qui le découvre peut l’exploiter pour injecter ses propres scripts.
Le hash fonctionne différemment : au lieu d’un token aléatoire, vous calculez l’empreinte SHA du contenu exact du script. Le navigateur compare le hash du script inline avec celui déclaré dans la CSP. Si ça correspond, le script s’exécute.
<script>
var message = "Bienvenue";
</script>
Pour générer le hash, vous calculez le SHA-256 du contenu du script (ici var message = "Bienvenue";) puis vous l’encodez en base64. La directive ressemble à ça :
Content-Security-Policy: script-src 'self' 'sha256-xyz...base64...'
Quand utiliser l’un ou l’autre ? Le nonce est plus adapté quand vos scripts inline changent régulièrement ou sont générés dynamiquement côté serveur. Le hash convient mieux aux scripts inline qui ne changent jamais (un snippet analytics figé, par exemple). Dans les deux cas, c’est nettement plus sûr que unsafe-inline.
Utiliser unsafe-eval
unsafe-eval autorise les fonctions comme eval(), new Function(), ou setTimeout avec une chaîne de caractères. Ces fonctions permettent d’exécuter du code JavaScript généré dynamiquement, ce qui est un vecteur d’attaque classique. Il faut éviter cette directive sauf nécessité absolue.
Oublier default-src
Sans default-src, toute directive non explicitement définie n’a aucune restriction. Si vous définissez script-src et style-src mais oubliez default-src, les images, polices, iframes et connexions réseau ne sont pas contrôlées. Un attaquant pourrait par exemple utiliser un élément <object> pour exécuter du code malveillant.
Utiliser des wildcards trop larges
script-src * ou script-src https: reviennent à ne rien restreindre du tout. Même script-src *.cdn.com peut poser problème si l’attaquant trouve un sous-domaine vulnérable ou un endpoint JSONP sur ce CDN.
Soyez aussi spécifique que possible dans vos sources autorisées. Listez les domaines exacts plutôt que d’utiliser des patterns trop permissifs.
Mettre en place CSP concrètement
La configuration se fait au niveau du serveur web. Voici les exemples les plus courants.
Nginx
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; object-src 'none'; frame-ancestors 'self';" always;
Apache
Header set Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; object-src 'none'; frame-ancestors 'self';"
Balise meta (alternative)
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; object-src 'none';">
Gardez en tête que la balise meta ne supporte pas frame-ancestors ni les directives de reporting. L’en-tête HTTP reste la méthode à privilégier.
L’approche itérative
Il faut éviter de vouloir tout verrouiller d’un coup. Voici une approche pragmatique :
- Commencez large :
Content-Security-Policy-Report-Only: default-src 'self' https: 'unsafe-inline'. Ça ne bloque rien mais vous donne une base de départ. - Analysez les rapports : identifiez les scripts tiers, les styles inline, les ressources externes légitimes.
- Restreignez progressivement : remplacez
https:par les domaines exacts, remplacez'unsafe-inline'par des nonces si possible. - Passez en mode actif : une fois la politique stabilisée, basculez sur
Content-Security-Policy. - Surveillez en continu : gardez le reporting actif même après le déploiement.
Maintenant que nous avons vu les bases, n’oubliez pas que CSP ne fonctionne pas en isolation. C’est une couche de défense parmi d’autres. Combinée avec HTTPS, HSTS, la validation des entrées, et des headers comme X-Frame-Options ou X-Content-Type-Options, elle participe à une stratégie de sécurité cohérente. CSP n’empêchera pas toutes les attaques, mais elle réduit considérablement la surface d’exploitation, et c’est déjà beaucoup.