Pourquoi la Content Security Policy mérite votre attention
La sécurité frontend est souvent abordée trop tard, quand l’application est déjà en production, que le HTML est généré par plusieurs sources, que des scripts tiers ont été ajoutés pour l’analytics, le support client ou la publicité, et que personne ne sait plus exactement quelles ressources sont autorisées.
La Content Security Policy, souvent abrégée CSP, permet de définir une politique de sécurité côté navigateur. Elle indique explicitement quelles sources de scripts, styles, images, polices, connexions réseau ou frames sont autorisées sur une page.
Elle ne remplace pas l’échappement HTML, la validation des entrées, l’authentification ou une architecture backend robuste. En revanche, elle constitue une couche de défense très utile contre certaines attaques XSS, les injections de scripts, les chargements de ressources non prévus et les erreurs d’intégration avec des services tiers.
Si vous travaillez déjà sur la robustesse de vos données avec le typage des API en TypeScript, voyez la CSP comme un équivalent côté navigateur : elle réduit la surface d’exécution au lieu de faire confiance à tout ce qui arrive dans la page.
Le principe : déclarer ce que le navigateur a le droit de charger
Une CSP se configure principalement via un en-tête HTTP :
Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'
Cette politique dit essentiellement :
- par défaut, seules les ressources du même domaine sont autorisées ;
- les scripts doivent venir du même domaine ;
- les anciens contenus embarqués via
object,embedouappletsont interdits ; - la balise
basene peut pas rediriger la résolution des URL vers un autre domaine.
Le navigateur applique cette règle avant d’exécuter ou de charger les ressources concernées. Si un script injecté tente de se charger depuis un domaine non autorisé, il est bloqué.
La documentation MDN sur Content-Security-Policy est une excellente référence pour explorer toutes les directives disponibles : MDN Content-Security-Policy.
Commencer avec une politique réaliste
Une erreur fréquente consiste à vouloir écrire une CSP parfaite immédiatement. Dans une application existante, cela casse souvent des scripts tiers, des styles inline, des images hébergées sur CDN ou des appels API oubliés.
Il est plus raisonnable de commencer avec le mode rapport :
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self'; object-src 'none'; report-to csp-endpoint
Avec Content-Security-Policy-Report-Only, le navigateur signale les violations sans bloquer la page. Cela permet d’observer ce que votre application charge réellement avant de durcir la règle.
Une politique de départ peut ressembler à ceci :
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data: https:; font-src 'self'; connect-src 'self' https://api.example.com; object-src 'none'; base-uri 'self'; frame-ancestors 'none'
Quelques directives importantes :
default-srcsert de valeur par défaut pour plusieurs types de ressources ;script-srccontrôle les scripts JavaScript ;style-srccontrôle les feuilles CSS et styles autorisés ;img-srccontrôle les images ;connect-srccontrôlefetch, WebSocket, EventSource et certains appels réseau ;frame-ancestorslimite les sites autorisés à intégrer votre page dans une iframe.
Cette dernière directive est particulièrement importante pour limiter certains scénarios de clickjacking.
Éviter le piège de unsafe-inline
Beaucoup de CSP finissent avec ceci :
Content-Security-Policy: script-src 'self' 'unsafe-inline'
Le problème est que 'unsafe-inline' réautorise précisément une grande partie de ce que l’on veut empêcher : l’exécution de scripts inline injectés dans le HTML.
Par exemple, si une faille permet d’injecter ce code dans une page :
<img src="x" onerror="fetch('/api/me').then(r => r.text()).then(console.log)" />
Une CSP stricte peut empêcher l’exécution du gestionnaire onerror. Avec 'unsafe-inline', la protection devient beaucoup moins intéressante.
La meilleure approche consiste à déplacer le JavaScript dans des fichiers dédiés :
<button class="menu-button" data-menu-target="main-menu">
Ouvrir le menu
</button>
document.querySelectorAll<HTMLButtonElement>('[data-menu-target]').forEach((button) => {
button.addEventListener('click', () => {
const targetId = button.dataset.menuTarget
if (!targetId) return
document.getElementById(targetId)?.classList.toggle('is-open')
})
})
Cette séparation rejoint les principes du progressive enhancement : le HTML reste lisible, le comportement est ajouté proprement, et la sécurité n’est pas sacrifiée pour un raccourci pratique.
Utiliser des nonces pour les scripts nécessaires
Certaines architectures imposent encore un script inline contrôlé par le serveur, par exemple pour injecter une configuration initiale. Dans ce cas, on peut utiliser un nonce : une valeur aléatoire générée pour chaque réponse HTTP.
Exemple d’en-tête :
Content-Security-Policy: script-src 'self' 'nonce-r4nd0mValue'; object-src 'none'; base-uri 'self'
Et dans le HTML généré :
<script nonce="r4nd0mValue">
window.__APP_CONFIG__ = {
locale: 'fr-FR',
apiBaseUrl: 'https://api.example.com'
}
</script>
Le navigateur n’exécutera ce script inline que si le nonce correspond à celui déclaré dans l’en-tête. Cette valeur doit être imprévisible et renouvelée à chaque réponse. Il ne faut donc jamais utiliser un nonce statique stocké dans le code source.
Dans une application Next.js, la génération et la transmission du nonce doivent être pensées avec le rendu serveur. Ce sujet croise directement les problématiques abordées dans React Server Components, car il faut distinguer ce qui est rendu côté serveur, ce qui est hydraté côté client et ce qui est réellement envoyé au navigateur.
Configurer une CSP dans un serveur Node.js
Voici un exemple minimal avec Express :
import express from 'express'
import crypto from 'node:crypto'
const app = express()
app.use((request, response, next) => {
const nonce = crypto.randomBytes(16).toString('base64')
response.locals.nonce = nonce
response.setHeader(
'Content-Security-Policy',
[
"default-src 'self'",
`script-src 'self' 'nonce-${nonce}'`,
"style-src 'self'",
"img-src 'self' data: https:",
"font-src 'self'",
"connect-src 'self' https://api.example.com",
"object-src 'none'",
"base-uri 'self'",
"frame-ancestors 'none'"
].join('; ')
)
next()
})
Dans un vrai projet, évitez de disperser cette configuration dans plusieurs fichiers. Centralisez les domaines autorisés, documentez pourquoi chaque source existe, et supprimez régulièrement les services tiers qui ne sont plus utilisés.
Sécuriser les appels réseau avec connect-src
La directive connect-src est souvent sous-estimée. Elle contrôle notamment les appels fetch, les WebSocket et les Server-Sent Events.
Si votre application utilise du temps réel avec Server-Sent Events, vous devrez autoriser explicitement l’origine concernée :
Content-Security-Policy: default-src 'self'; connect-src 'self' https://api.example.com https://events.example.com
Même logique pour une architecture qui utilise un cache applicatif, des appels API centralisés ou une stratégie stale-while-revalidate, comme dans le cache frontend. La CSP doit refléter les flux réseau réels de l’application.
Attention aux ressources tierces
Une CSP efficace force à poser une question saine : quels domaines ont vraiment le droit d’exécuter du code dans mes pages ?
Les scripts tiers sont pratiques, mais ils ont souvent des privilèges importants. Un script d’analytics, de chat ou d’A/B testing peut lire une partie du DOM, observer des interactions et parfois injecter d’autres ressources.
Il est donc préférable de :
- limiter les domaines autorisés au strict nécessaire ;
- éviter les jokers comme
https:dansscript-src; - surveiller les scripts chargés dynamiquement ;
- documenter chaque exception ;
- tester la politique en préproduction.
Les import maps, présentées dans Import maps JavaScript, peuvent aussi bénéficier d’une CSP claire : si vous chargez des modules depuis un CDN, ce choix doit être assumé dans script-src plutôt que caché dans une configuration implicite.
Tester et faire évoluer la politique
Une CSP n’est pas un fichier que l’on écrit une fois pour toutes. Elle évolue avec l’application.
Pour la tester, commencez par les DevTools du navigateur : les violations CSP apparaissent dans la console. Ensuite, configurez un endpoint de rapport pour collecter les violations en environnement réel.
Un exemple de configuration moderne avec Report-To peut ressembler à ceci :
{
"group": "csp-endpoint",
"max_age": 10886400,
"endpoints": [
{
"url": "https://reports.example.com/csp"
}
]
}
Côté serveur, vous pouvez stocker ces rapports, les échantillonner, puis les analyser pour distinguer une vraie erreur de configuration d’une extension navigateur ou d’un script injecté localement.
Bonnes pratiques à retenir
Une bonne Content Security Policy n’est pas forcément la plus longue. C’est celle qui décrit précisément votre application.
Commencez par default-src 'self', interdisez object-src, limitez frame-ancestors, évitez 'unsafe-inline', utilisez des nonces lorsque c’est nécessaire, et faites évoluer la politique à partir de rapports réels.
La CSP impose aussi une discipline d’architecture : moins de scripts inline, moins de dépendances opaques, des ressources mieux identifiées et une séparation plus nette entre HTML, CSS et JavaScript. Pour une application web moderne, c’est une contrainte saine : elle rend les erreurs visibles avant qu’elles ne deviennent des failles difficiles à corriger.