Content Security Policy (CSP) est l’un des principaux mécanismes de sécurité proposés par les navigateurs pour limiter l’impact des vulnérabilités côté client. En théorie, une CSP bien conçue neutralise une grande partie des attaques par injection ; en pratique, elle est souvent affaiblie par des compromis accumulés au fil du temps : exceptions ajoutées pour préserver la compatibilité avec du code hérité, allowlists de domaines tiers qui s’élargissent sans jamais être nettoyées, nonces mal générés, ou encore relations de confiance mal maîtrisées. Résultat : une politique qui paraît stricte à première lecture peut, dans certaines conditions, rester parfaitement contournable.
Cet article revient sur le fonctionnement de CSP, ses principales directives et expressions de sources, les erreurs de configuration les plus courantes et les techniques de contournement. Nous verrons ensuite comment auditer une CSP en conditions de pentest, puis comment construire une politique réellement robuste face à ces attaques.
Guide complet sur Content Security Policy (CSP)
- Qu’est-ce que Content Security Policy ?
- Quel est le rôle de CSP dans la prévention des XSS ?
- Principes et fonctionnement de Content Security Policy
- Comprendre les principales directives CSP
- Comprendre les expressions de sources et les valeurs CSP
- Trusted Types et réduction de la surface DOM XSS
- Les limites de Content Security Policy
- Quelles sont les erreurs de configuration CSP les plus fréquentes ?
- Dépendance excessive à unsafe-inline
- Dépendance excessive à unsafe-eval
- Wildcards et allowlists de domaines trop larges
- Confiance excessive dans 'self'
- Politique de base incomplète
- Absence explicite de object-src et base-uri
- Nonces statiques, prévisibles ou mal attribués
- CSP uniquement déployée en Report-Only
- Déploiement incohérent selon les routes
- Compromis de compatibilité avec les navigateurs anciens
- Techniques courantes de contournement de CSP
- Exploitation d’une politique autorisant unsafe-inline
- Domaine tiers de confiance et endpoint JSONP
- Contournement de script-src 'self' via un upload
- Contournement d’un nonce faible ou mal utilisé
- strict-dynamic et loader de script vulnérable
- Script gadget, framework de confiance et DOM clobbering
- Injection HTML sans exécution JavaScript, mais impact via base-uri ou form-action
- DOM XSS, CSP et Trusted Types : bien distinguer les mécanismes
- Méthodologie de pentest d’une Content Security Policy
- Comment construire une CSP robuste ?
- Commencer par les besoins fonctionnels réels
- Privilégier les nonces ou hashes pour JavaScript
- Interdire explicitement les event handlers inline lorsque cela est possible
- Supprimer progressivement unsafe-inline et unsafe-eval
- Minimiser les domaines tiers
- Isoler les contenus contrôlés par les utilisateurs
- Utiliser base-uri, form-action, frame-ancestors et object-src explicitement
- Adopter Trusted Types lorsque l’architecture le permet
- Déployer la politique progressivement avec Report-Only
- Surveiller les violations sans transformer le reporting en bruit
- Maintenir la politique dans le temps
- Conclusion
Qu’est-ce que Content Security Policy ?
Content Security Policy est un mécanisme de sécurité appliqué directement par le navigateur. Une application transmet au navigateur une politique qui décrit les sources et comportements autorisés pour le document courant. Le navigateur vérifie ensuite chaque opération concernée par CSP avant de l’autoriser.
Une politique minimale peut par exemple être envoyée dans la réponse HTTP :
Content-Security-Policy: default-src 'self'; script-src 'self'
Cette configuration indique que, par défaut, les ressources doivent provenir de la même origine que l’application et que les scripts JavaScript doivent eux aussi être chargés depuis une source correspondant à 'self'.
Si la page contient ensuite :
<script src="/js/app.js"></script>
le navigateur autorisera normalement le chargement puisque la ressource provient de l’origine courante. En revanche :
<script src="https://attacker.example/payload.js"></script>
sera bloqué si attacker.example n’est pas autorisé par la directive effectivement applicable au script.
CSP n’essaye pas de déterminer si un fichier JavaScript est malveillant. Le navigateur ne réalise pas une analyse sémantique du code pour décider si celui-ci est dangereux. Il vérifie si le chargement ou l’exécution demandés respectent les règles qui lui ont été fournies. Cette distinction est essentielle : CSP constitue avant tout un système de contrôle des capacités et des relations de confiance du document.
Quel est le rôle de CSP dans la prévention des XSS ?
CSP est fréquemment présentée comme une protection contre les XSS. Cette formulation est correcte à condition de comprendre précisément ce qu’elle signifie. Une CSP ne corrige pas la vulnérabilité qui permet à une donnée contrôlée d’être injectée dans le HTML ou le DOM. Elle peut en revanche rendre certains chemins d’exploitation inopérants.
Imaginons qu’une application réinjecte sans encodage une valeur contrôlée par un utilisateur :
<div>
USER_INPUT
</div>
Si l’utilisateur parvient à transformer cette valeur en HTML arbitraire, la vulnérabilité existe indépendamment de CSP. Une politique restrictive peut néanmoins empêcher certaines charges utiles d’être exécutées. Avec :
Content-Security-Policy: script-src 'self'
une injection de JavaScript inline sera normalement refusée si aucun autre mécanisme de la politique ne l’autorise.
Il serait cependant dangereux d’en déduire que l’injection n’a plus d’impact. L’attaquant peut parfois trouver une autre primitive : charger un script depuis une source déjà autorisée, détourner un loader JavaScript de confiance, exploiter un script gadget fourni par un framework, modifier la destination d’un formulaire, influencer une URL de base ou tirer parti d’un comportement du DOM qui n’exige pas l’introduction d’un nouveau bloc <script>.
CSP doit donc rester une mesure de défense en profondeur. La prévention à la source repose toujours sur l’encodage contextuel des sorties, la sanitisation lorsque du HTML est légitimement accepté, l’utilisation d’API DOM sûres et la suppression des flux qui transforment une donnée non fiable en code ou en contenu interprété.
Principes et fonctionnement de Content Security Policy
Lorsqu’un navigateur reçoit un document protégé par CSP, il analyse les politiques associées au document et construit l’ensemble des règles qu’il devra appliquer pendant son utilisation. Lorsqu’une ressource doit être récupérée, lorsqu’un script doit être exécuté, lorsqu’un formulaire est soumis ou lorsqu’une autre opération relevant de CSP se produit, le navigateur détermine la directive applicable puis vérifie si l’action respecte la politique.
Prenons la configuration suivante :
Content-Security-Policy:
default-src 'self';
script-src 'self' https://static.example.com;
img-src 'self' https://images.example.com;
connect-src 'self' https://api.example.com
Un script provenant de https://static.example.com/app.js peut être autorisé, alors qu’un script provenant de https://attacker.example/payload.js est bloqué. De la même manière, une requête fetch() vers https://api.example.com/users peut être autorisée par connect-src, tandis qu’une requête vers une origine non déclarée est refusée.
Ce mécanisme fait du navigateur une couche d’enforcement supplémentaire. Il n’empêche pas l’application de générer un HTML vulnérable, mais il peut refuser certaines conséquences que ce HTML tenterait de provoquer.
L’en-tête HTTP Content-Security-Policy
Le mécanisme recommandé pour déployer une politique est l’en-tête HTTP :
Content-Security-Policy: default-src 'self'
Cet en-tête accompagne la réponse contenant le document à protéger. Il permet d’appliquer la politique dès le chargement et donne accès à l’ensemble des directives prévues pour ce mode de livraison.
Une politique peut également être déclarée dans le document HTML :
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'">
Cette méthode peut être utile lorsqu’il est impossible de modifier les en-têtes HTTP, mais elle possède plusieurs limitations. Content-Security-Policy-Report-Only ne peut pas être déployé via une balise <meta>, et des directives comme frame-ancestors, report-uri ou sandbox ne sont pas prises en charge dans ce mode. Une politique déclarée avec <meta> ne s’applique en outre qu’aux contenus situés après l’élément une fois celui-ci interprété. Dès que l’infrastructure le permet, l’en-tête HTTP doit donc être privilégié.
Processus de validation dans le navigateur
La directive applicable dépend du type d’opération. Pour un élément <script>, le navigateur peut par exemple utiliser script-src-elem, puis se rabattre sur script-src et enfin default-src si les directives plus spécifiques sont absentes. Pour un gestionnaire d’évènement inline comme onclick, script-src-attr peut s’appliquer. Pour une connexion fetch() ou WebSocket, le navigateur examine connect-src, puis default-src si connect-src n’est pas défini.
Certaines directives suivent toutefois une logique différente. base-uri, form-action et frame-ancestors ne récupèrent pas leur valeur depuis default-src. Une politique qui définit default-src 'none' sans définir ces directives ne doit donc pas être interprétée comme bloquant automatiquement les opérations qu’elles contrôlent.
Cette notion de fallback est l’un des points les plus importants à maîtriser lors d’un audit. L’objectif n’est pas de lire une chaîne de caractères, mais de reconstruire la politique effectivement appliquée à chaque comportement.
Politiques CSP multiples
Une même réponse peut contenir plusieurs politiques CSP. Elles ne se remplacent pas : le navigateur doit les appliquer conjointement.
Considérons :
Content-Security-Policy: default-src 'self'; connect-src 'none'
Content-Security-Policy: script-src https://static.example.com; connect-src https://api.example.com
La seconde politique autorise théoriquement https://api.example.com pour connect-src, mais la première contient connect-src 'none'. Une connexion vers cette API reste donc bloquée, car elle doit satisfaire toutes les politiques en mode enforcement.
Ce comportement est important en pentest. Fusionner naïvement deux CSP ou ne lire que le dernier header peut conduire à une conclusion erronée sur les ressources réellement autorisées.
Content-Security-Policy et Content-Security-Policy-Report-Only
CSP possède deux modes de déploiement principaux. Avec :
Content-Security-Policy: default-src 'self'
les violations sont effectivement bloquées. Avec :
Content-Security-Policy-Report-Only: default-src 'self'
le navigateur évalue la politique et génère éventuellement des rapports, mais ne bloque pas l’opération à cause de cette politique Report-Only.
Cette distinction est fondamentale. Report-Only est particulièrement utile lors de l’introduction d’une CSP, car il permet d’observer les ressources légitimes qui seraient bloquées, d’ajuster la politique puis d’activer progressivement l’enforcement.
Les deux modes peuvent coexister. Une application peut conserver une politique stable en enforcement et tester en parallèle une politique plus restrictive en Report-Only. Lors d’un audit, il faut donc toujours identifier quelle politique protège réellement la page et quelle politique ne fait qu’observer son comportement.
Comprendre les principales directives CSP
Une politique CSP est composée de directives qui contrôlent des catégories de ressources ou des comportements spécifiques. Certaines sont des directives de récupération, d’autres protègent la structure ou la navigation du document, d’autres encore concernent le reporting ou des mécanismes modernes comme Trusted Types.
Il est important de ne pas considérer CSP comme une liste de mots-clés indépendants. Les directives forment un ensemble cohérent, avec des mécanismes de fallback et des interactions qui influencent la politique effective.
Directives de récupération
Les directives de récupération déterminent les sources autorisées pour différents types de ressources. default-src joue souvent le rôle de valeur de repli, mais une directive plus spécifique remplace ce fallback pour la catégorie qu’elle contrôle.
default-src
default-src fournit une politique de repli pour de nombreuses catégories de ressources.
Content-Security-Policy: default-src 'self'
Si aucune directive plus spécifique n’existe, les ressources concernées doivent correspondre à ‘self’. Avec :
Content-Security-Policy:
default-src 'self';
img-src https://images.example.com
les images sont contrôlées par img-src, tandis que d’autres ressources couvertes par le mécanisme de fallback restent soumises à default-src 'self'.
Une stratégie de durcissement courante consiste à démarrer avec :
Content-Security-Policy: default-src 'none'
puis à réautoriser explicitement les catégories nécessaires. Cette approche réduit les autorisations implicites, mais elle ne dispense pas de définir les directives qui ne dépendent pas de default-src, notamment base-uri, form-action et frame-ancestors.
script-src
script-src est généralement la directive la plus sensible d’une CSP, car elle contrôle une grande partie des mécanismes permettant l’exécution de JavaScript.
Content-Security-Policy: script-src 'self'
Cette politique autorise normalement les scripts chargés depuis l’origine courante et bloque ceux provenant d’origines non autorisées. Elle bloque également, en l’absence d’exception, l’exécution classique de scripts inline et de nombreux mécanismes d’évaluation dynamique.
L’analyse de script-src ne doit cependant jamais se limiter à la liste des domaines. Les nonces, hashes, strict-dynamic, 'unsafe-inline', 'unsafe-eval', 'wasm-unsafe-eval', ainsi que les directives plus spécifiques script-src-elem et script-src-attr, peuvent modifier profondément le modèle de confiance.
script-src-elem et script-src-attr
CSP Level 3 permet de distinguer plus finement les scripts présents dans des éléments <script> des gestionnaires d’évènements inline.
script-src-elem s’applique aux éléments <script>, qu’il s’agisse de scripts externes ou de blocs inline. Par exemple :
Content-Security-Policy:
script-src 'self';
script-src-elem https://static.example.com
Dans ce cas, les éléments <script> sont évalués selon script-src-elem. Si cette directive est absente, le navigateur se rabat sur script-src, puis sur default-src si nécessaire.
script-src-attr s’applique aux gestionnaires d’évènements JavaScript déclarés dans des attributs HTML, tels que :
<button onclick="save()">Enregistrer</button>
Une application moderne peut explicitement interdire ces handlers :
Content-Security-Policy:
script-src 'nonce-RANDOM' 'strict-dynamic';
script-src-attr 'none'
Cette séparation est importante lors d’un audit, car une politique peut être stricte pour les éléments <script> tout en conservant des règles différentes pour les attributs exécutables.
style-src, style-src-elem et style-src-attr
style-src contrôle les feuilles de style et, selon la configuration, le CSS inline. CSP3 prévoit également style-src-elem pour les éléments et feuilles de style, ainsi que style-src-attr pour les attributs style.
Content-Security-Policy: style-src 'self'
Les styles présentent généralement moins de risques que JavaScript en matière d’exécution de code, mais ils ne doivent pas être considérés comme dépourvus d’impact. Une injection CSS peut modifier l’interface de manière trompeuse et, dans certains scénarios, contribuer à des fuites d’informations ou à la construction d’un canal auxiliaire. Une politique robuste limite donc les styles aux sources réellement nécessaires.
img-src
img-src définit les sources autorisées pour les images.
Content-Security-Policy:
img-src 'self' https://images.example.com
Le contrôle des images est utile au-delà de l’hygiène générale. Le chargement d’une image provoque une requête réseau et peut être utilisé dans certains scénarios comme canal d’exfiltration ou de signalement. Restreindre img-src réduit donc les capacités offertes à un contenu injecté, même lorsque ce contenu ne peut pas exécuter directement du JavaScript.
connect-src
connect-src contrôle les destinations auxquelles le document peut se connecter via des mécanismes comme fetch(), XMLHttpRequest, WebSocket, EventSource ou certaines opérations reposant sur Beacon.
Content-Security-Policy:
connect-src 'self' https://api.example.com
Un script exécuté dans la page pourra contacter l’API autorisée, mais une tentative de connexion directe vers une origine non prévue pourra être bloquée. Cette directive constitue une mesure de défense en profondeur intéressante contre certaines formes d’exfiltration, mais elle ne doit pas être interprétée comme une interdiction universelle de toute sortie réseau : d’autres types de ressources ou de navigation sont contrôlés par d’autres directives.
frame-src et child-src
frame-src définit les origines pouvant être chargées dans des éléments comme <iframe>.
Content-Security-Policy:
frame-src https://player.example.com
Cette directive ne doit pas être confondue avec frame-ancestors. frame-src contrôle ce que la page peut intégrer ; frame-ancestors contrôle quelles pages ont le droit d’intégrer la page courante.
child-src est une directive plus ancienne qui peut encore servir de fallback pour certains contextes, notamment lorsque frame-src ou worker-src ne sont pas définis. Dans une politique moderne, il est préférable de définir explicitement les directives correspondant aux usages réellement nécessaires plutôt que de s’appuyer sur des comportements de repli difficiles à lire.
worker-src
worker-src contrôle les sources utilisées pour créer des Workers, SharedWorkers et Service Workers.
Content-Security-Policy:
worker-src 'self'
Lorsque worker-src est absente, la recherche d’une directive applicable peut passer par child-src, puis script-src, puis default-src. Cette chaîne de fallback explique pourquoi une politique qui semble stricte au premier regard peut autoriser ou bloquer des workers d’une manière différente de celle imaginée par les développeurs.
Dans les applications modernes qui utilisent des Service Workers, des workers de calcul ou des architectures frontend complexes, cette directive mérite d’être configurée explicitement.
font-src, media-src et manifest-src
font-src limite l’origine des polices web. media-src s’applique aux contenus audio et vidéo, tandis que manifest-src contrôle les manifestes d’applications web.
Ces directives sont généralement moins critiques que script-src, mais elles participent au principe de moindre privilège. Une application ne devrait autoriser que les catégories de ressources et les origines dont elle a réellement besoin.
object-src
object-src contrôle les ressources utilisées par les éléments <object> et <embed>. Ces mécanismes sont aujourd’hui moins courants et reposent historiquement sur des technologies à forte surface d’attaque.
Une politique moderne définit donc fréquemment :
Content-Security-Policy: object-src 'none'
Il faut néanmoins apporter une nuance importante. object-src possède un fallback vers default-src. Son absence n’implique donc pas nécessairement une absence totale de restriction si default-src est déjà restrictif. Définir explicitement object-src 'none' reste toutefois une bonne pratique lorsque ces ressources ne sont pas nécessaires, car cela exprime clairement l’intention de sécurité et évite qu’un futur assouplissement de default-src ne réintroduise involontairement cette capacité.
Directives liées au document et à la navigation
Certaines directives importantes ne contrôlent pas directement un fichier JavaScript, une image ou une feuille de style. Elles agissent sur la structure du document, les destinations de navigation ou la manière dont la page peut être intégrée.
base-uri
La balise HTML <base> permet de modifier la base utilisée par le navigateur pour résoudre les URLs relatives d’un document. Une injection telle que :
<base href="https://attacker.example/">
peut donc modifier la résolution de références relatives présentes plus loin dans la page. Pour empêcher ce comportement lorsque l’application n’utilise pas <base> :
Content-Security-Policy: base-uri 'none'
constitue un choix robuste. Lorsque cette fonctionnalité est nécessaire, base-uri 'self' peut constituer une alternative plus permissive. base-uri ne récupère pas sa valeur depuis default-src, ce qui signifie que son absence doit être analysée explicitement.
form-action
form-action définit les destinations autorisées pour les soumissions de formulaires.
Content-Security-Policy: form-action 'self'
Cette directive est particulièrement intéressante lorsqu’une injection HTML subsiste sans possibilité d’exécuter du JavaScript. Un attaquant peut parfois tenter d’injecter ou de modifier un formulaire afin de transmettre des données à une autre origine. form-action permet de réduire cette surface.
Comme base-uri, cette directive ne possède pas de fallback vers default-src. Une politique default-src 'none' ne bloque donc pas automatiquement toutes les destinations de formulaires.
frame-ancestors
frame-ancestors contrôle les origines autorisées à intégrer la page courante dans une frame.
Content-Security-Policy: frame-ancestors 'none'
interdit toute intégration, tandis que :
Content-Security-Policy: frame-ancestors 'self'
limite l’intégration aux origines correspondant à 'self'.
Cette directive constitue un mécanisme moderne de protection contre le clickjacking et offre davantage de flexibilité que l’ancien en-tête X-Frame-Options. Elle n’a pas de fallback vers default-src et doit donc être définie explicitement lorsque la protection contre le framing est attendue.
sandbox
La directive sandbox applique au document des restrictions comparables à celles de l’attribut sandbox d’une iframe. Selon les tokens explicitement réautorisés, elle peut limiter l’exécution de scripts, les formulaires, la navigation, les popups ou certaines caractéristiques de l’origine.
Cette directive n’est pas adaptée à toutes les pages, mais elle peut être utile pour isoler des contenus intrinsèquement moins fiables. Elle doit être utilisée avec prudence, car une configuration trop restrictive peut casser de nombreuses fonctionnalités et une configuration trop permissive peut donner un faux sentiment d’isolation.
upgrade-insecure-requests
La directive :
Content-Security-Policy: upgrade-insecure-requests
indique au navigateur de traiter certaines URLs HTTP comme si elles avaient été remplacées par leurs équivalents HTTPS. Elle peut faciliter la migration d’une application historique contenant encore des références non sécurisées.
Elle ne remplace toutefois ni le déploiement correct de HTTPS ni HSTS. Son rôle est de faciliter la transition et de réduire certaines formes de mixed content, pas de constituer à elle seule une stratégie de transport sécurisé.
Les directives de reporting
Le reporting CSP permet d’obtenir de la visibilité sur les opérations que la politique bloque ou bloquerait. Il est particulièrement utile lors du déploiement progressif d’une politique ou pour suivre l’apparition de nouvelles dépendances.
report-to et Reporting-Endpoints
Le mécanisme moderne repose sur un endpoint déclaré séparément :
Reporting-Endpoints: csp-endpoint="https://reports.example.com/csp"
puis référencé depuis la CSP :
Content-Security-Policy:
default-src 'self';
report-to csp-endpoint
report-to contient le nom logique de l’endpoint, tandis que Reporting-Endpoints associe ce nom à une URL. Lorsqu’une violation est observée, le navigateur peut envoyer un rapport contenant notamment la directive effective, la ressource bloquée et le mode enforce ou report concerné.
Le reporting doit être traité comme une source de données potentiellement contrôlée par un attaquant. Les valeurs collectées ne doivent pas être affichées sans encodage ou contrôle dans un tableau de bord interne.
report-uri
report-uri est le mécanisme historique de reporting CSP. Il est désormais obsolète au profit de report-to et de l’API Reporting, mais demeure utile pour certains navigateurs ou environnements plus anciens.
Pendant une phase de transition, une application peut donc déclarer les deux mécanismes. Il faut toutefois garder à l’esprit qu’un navigateur prenant en charge report-to peut privilégier ce mécanisme et ignorer report-uri pour la politique concernée.
Comprendre les expressions de sources et les valeurs CSP
Une directive ne suffit pas à déterminer si une politique est restrictive. La sécurité réelle dépend de la manière dont les sources et les mots-clés sont exprimés.
Ainsi, les politiques suivantes utilisent toutes script-src mais construisent des modèles de confiance très différents :
Content-Security-Policy: script-src *
Content-Security-Policy: script-src 'self'
Content-Security-Policy: script-src 'nonce-RANDOM' 'strict-dynamic'
La première s’appuie sur une autorisation extrêmement large de sources réseau. La seconde fait confiance à l’origine de l’application. La troisième identifie des scripts racines précis au moyen d’un nonce et peut propager cette confiance aux scripts qu’ils chargent dynamiquement.
‘self’
La valeur 'self' désigne l’origine protégée et prend notamment en compte le schéma, l’hôte et le port, avec certaines règles d’upgrade sécurisé prévues par CSP. Une politique :
Content-Security-Policy: script-src 'self'
permet par exemple de charger un script relatif comme /js/app.js lorsqu’il appartient à l’origine correspondante.
Il serait toutefois incorrect d’assimiler automatiquement 'self' à une ressource sûre. Si l’application sert également, depuis cette même origine, des fichiers contrôlés par les utilisateurs, des contenus générés dynamiquement ou des ressources issues d’un stockage insuffisamment cloisonné, la frontière de confiance peut être trop large. Le navigateur ne sait pas qu’un fichier situé dans /uploads/ est « utilisateur » tandis qu’un fichier situé dans /js/ est « applicatif » : si les deux correspondent à la même source CSP, ils appartiennent au même périmètre de confiance.
‘none’
La valeur ‘none’ représente l’ensemble vide pour la directive concernée.
Content-Security-Policy: object-src 'none'
interdit ainsi les ressources contrôlées par object-src.
'none' doit être utilisée seule dans une liste de sources pour exprimer cette interdiction. Si elle est mélangée à d’autres expressions de sources, ces autres expressions peuvent continuer à correspondre ; il ne faut donc pas écrire une configuration ambiguë en pensant que 'none' l’emportera sur tout le reste.
Sources basées sur un hôte
Une host source peut contenir un schéma, un hôte, un port et éventuellement un chemin.
script-src https://static.example.com
fait confiance aux ressources correspondant à cette source. L’hôte peut également être restreint à un chemin :
script-src https://static.example.com/js/
Les chemins terminés par / fonctionnent comme des préfixes, tandis qu’un chemin sans / final peut être comparé plus strictement. Il faut cependant garder en tête que les redirections possèdent une sémantique particulière : après une redirection, la partie chemin de l’expression n’est pas utilisée de la même manière pour le matching. Une restriction de chemin ne doit donc pas être considérée comme une frontière de sécurité équivalente à une séparation d’origine.
L’autorisation d’un hôte implique également une question fondamentale : qui peut servir du contenu depuis cet hôte ? Un domaine appartenant à un fournisseur réputé peut rester dangereux dans script-src s’il s’agit d’une plateforme mutualisée où un utilisateur arbitraire peut publier du JavaScript.
Wildcards
Les wildcards permettent d’élargir la portée d’une expression. Par exemple :
script-src https://*.example.com
autorise un ensemble de sous-domaines correspondant à l’expression.
Une telle règle ne doit pas être analysée uniquement à partir du domaine principal. Les environnements de développement oubliés, sous-domaines délégués à des tiers, buckets de stockage, espaces clients ou services SaaS intégrés peuvent tous devenir pertinents.
La wildcard globale * est encore plus permissive pour les sources réseau compatibles avec la directive concernée. Il faut néanmoins éviter de la décrire comme « absolument toutes les sources possibles » : des schémas spéciaux tels que data: ou blob: possèdent leur propre traitement et doivent souvent être autorisés explicitement. La bonne conclusion de sécurité reste que * élargit massivement le périmètre de confiance et n’a généralement pas sa place dans script-src d’une politique visant à limiter les XSS.
Sources basées sur un schéma
Une source comme :
img-src https:
autorise les ressources correspondant au schéma HTTPS, sans limiter l’hôte. Ce niveau de confiance peut être acceptable pour certaines catégories peu sensibles selon le contexte, mais serait beaucoup plus dangereux avec :
script-src https:
puisque le navigateur pourrait alors considérer comme admissibles des scripts provenant d’un très grand nombre d’origines HTTPS.
Les politiques modernes doivent donc préférer des sources aussi précises que possible, surtout pour JavaScript.
data:, blob: et autres schémas spéciaux
Les schémas data: et blob: sont parfois nécessaires à des applications frontend qui génèrent localement des images, des téléchargements ou d’autres ressources. Leur autorisation doit cependant être limitée aux directives qui en ont réellement besoin.
Par exemple, autoriser :
img-src 'self' data:
peut être justifié pour des images inline, tandis que réutiliser la même logique pour script-src introduirait une surface beaucoup plus sensible.
blob: mérite également une attention spécifique. Selon la catégorie de ressource et le navigateur, un Blob peut devenir un objet exécutable ou charger du code dans un worker. Une politique robuste n’ajoute donc pas data: ou blob: par réflexe ; elle documente leur nécessité pour chaque type de ressource.
Les nonces
Un nonce CSP est une valeur aléatoire associée à une réponse donnée et utilisée pour identifier des éléments explicitement autorisés.
La réponse peut contenir :
Content-Security-Policy:
script-src 'nonce-a8Dk3mQ91Yx7...'
et le document :
<script nonce="a8Dk3mQ91Yx7...">
initialiseApplication();
</script>
Le navigateur compare le nonce de l’élément à celui de la politique. Si les valeurs correspondent et que les autres conditions CSP sont satisfaites, le script est autorisé.
L’intérêt de ce modèle est majeur : la confiance n’est plus accordée à l’intégralité d’un domaine mais directement à certains scripts approuvés par l’application pour cette réponse.
Pour être efficace, un nonce doit être généré côté serveur avec une source aléatoire cryptographiquement sûre, posséder une entropie suffisante et être renouvelé à chaque réponse. Une valeur statique, prévisible ou dérivée d’un timestamp ne fournit pas la propriété attendue.
La qualité cryptographique ne suffit toutefois pas. Si le moteur de rendu ajoute automatiquement le nonce à un élément <script> dont une partie est contrôlée par un attaquant, la politique peut continuer à faire confiance au mauvais contenu. Il faut donc auditer à la fois la génération du nonce et les endroits où celui-ci est appliqué.
Les hashes
Les hashes permettent d’autoriser un contenu précis plutôt qu’un domaine ou une valeur renouvelée à chaque réponse.
Content-Security-Policy:
script-src 'sha256-BASE64_HASH'
Le navigateur calcule l’empreinte du script concerné et vérifie qu’elle correspond à celle déclarée dans la politique. Cette approche est adaptée aux blocs inline statiques dont le contenu change peu.
Les hashes offrent une granularité forte, mais leur maintenance peut devenir coûteuse si l’application modifie fréquemment son HTML ou génère du code inline dynamique. Ils peuvent également participer à une Strict CSP avec strict-dynamic, où le script correspondant au hash devient une racine de confiance.
strict-dynamic
strict-dynamic modifie profondément la manière dont script-src établit la confiance.
Considérons :
Content-Security-Policy:
script-src 'nonce-RANDOM' 'strict-dynamic'
et un script racine :
<script nonce="RANDOM" src="/js/loader.js"></script>
Le navigateur fait confiance à ce script parce qu’il possède le nonce attendu. Lorsque ce script charge ensuite programmatiquement d’autres scripts, la confiance peut être propagée à ces nouveaux scripts dans les conditions prévues par CSP.
Par exemple :
const script = document.createElement("script");
script.src = "/js/module.js";
document.head.appendChild(script);
L’objectif est d’éviter de maintenir des allowlists de domaines volumineuses pour des applications qui utilisent des loaders ou des bundles dynamiques.
Dans les navigateurs CSP3, lorsque strict-dynamic est actif avec une racine de confiance basée sur nonce ou hash, les host sources, scheme sources, 'self' et 'unsafe-inline' présents dans la directive concernée sont ignorés pour le modèle moderne. Cela permet notamment de construire des politiques rétrocompatibles où les anciens navigateurs utilisent les allowlists tandis que les navigateurs récents reposent sur nonce + strict-dynamic.
Cette propriété implique toutefois une responsabilité importante : les scripts racines deviennent des composants critiques de sécurité. Si un script autorisé crée un nouvel élément <script> à partir d’une URL contrôlée par un utilisateur, l’attaquant peut chercher à détourner la confiance déjà accordée plutôt qu’à injecter lui-même un script non autorisé.
unsafe-inline
'unsafe-inline' réautorise des formes de contenu inline que CSP cherche normalement à empêcher, notamment les blocs <script> et, selon la directive effectivement appliquée, certains gestionnaires d’évènements inline.
Content-Security-Policy:
script-src 'self' 'unsafe-inline'
peut donc rendre de nombreuses injections HTML directement exploitables en JavaScript.
Il faut cependant éviter une conclusion trop simpliste. Dans une politique moderne qui utilise des nonces ou des hashes, le navigateur peut ignorer 'unsafe-inline' pour les scripts correspondants. Avec strict-dynamic, cette valeur est également ignorée par les navigateurs CSP3 dans le modèle de confiance moderne. script-src-attr peut en outre imposer une règle spécifique aux event handlers.
Lors d’un pentest, la présence textuelle de 'unsafe-inline' est donc un signal à analyser, pas une preuve automatique de bypass.
unsafe-hashes
'unsafe-hashes' facilite certains scénarios de migration où une application veut conserver des gestionnaires d’évènements inline précis sans réactiver l’ensemble de 'unsafe-inline'. Le mécanisme permet à certains attributs exécutables d’être autorisés via leurs hashes.
Cette approche peut être utile temporairement, mais le code moderne devrait autant que possible remplacer :
<button onclick="save()">Enregistrer</button>
par une association réalisée dans du JavaScript autorisé :
button.addEventListener("click", save);
Cela réduit la dépendance au code exécutable directement mélangé au HTML et facilite l’adoption d’une politique stricte.
unsafe-eval
'unsafe-eval' autorise différents mécanismes qui construisent ou exécutent du code à partir de chaînes de caractères, notamment eval() et Function().
Content-Security-Policy:
script-src 'self' 'unsafe-eval'
n’introduit pas à lui seul une XSS. Il supprime toutefois une barrière qui aurait pu bloquer une primitive d’évaluation dangereuse.
Par exemple :
const expression = getUserControlledValue();
eval(expression);
est vulnérable parce qu’une donnée non fiable atteint un mécanisme d’exécution. La présence de 'unsafe-eval' permet alors à l’exploitation de se produire malgré la CSP.
Les dépendances qui nécessitent encore 'unsafe-eval' en production doivent être identifiées et, si possible, reconfigurées ou remplacées.
wasm-unsafe-eval
Les applications WebAssembly peuvent avoir besoin d’autoriser certains mécanismes de compilation dynamique. 'wasm-unsafe-eval' fournit une autorisation plus ciblée que 'unsafe-eval' pour ce besoin.
Lorsque seul WebAssembly dynamique est nécessaire, cette valeur est préférable à l’activation beaucoup plus large de 'unsafe-eval'. Si 'unsafe-eval' est déjà présent, il autorise néanmoins davantage de mécanismes et rend cette granularité moins utile.
Trusted Types et réduction de la surface DOM XSS
Trusted Types complète CSP en s’attaquant à une problématique différente de la simple sélection des sources. L’objectif est de réduire le nombre d’endroits où une chaîne de caractères arbitraire peut être transmise à des sinks DOM capables d’interpréter du HTML, du script ou des URLs de script.
Considérons :
element.innerHTML = userInput;
Une CSP stricte peut bloquer certains payloads produits par cette affectation, mais le sink dangereux reste présent. Trusted Types permet de demander au navigateur que certaines APIs n’acceptent plus directement des chaînes et qu’elles reçoivent des objets typés produits par des policies explicitement créées par l’application.
require-trusted-types-for ‘script’
Une application peut activer :
Content-Security-Policy:
require-trusted-types-for 'script'
Le navigateur applique alors les exigences Trusted Types aux sinks DOM concernés. Une affectation directe d’une chaîne dans un sink protégé peut provoquer une erreur si la valeur n’a pas été produite par une Trusted Types policy appropriée.
Depuis février 2026, cette directive est disponible sur les versions récentes des principaux navigateurs, même si la compatibilité avec des clients plus anciens doit toujours être prise en compte dans une stratégie de déploiement.
La directive trusted-types
La directive trusted-types contrôle les noms de policies que le code est autorisé à créer.
Content-Security-Policy:
require-trusted-types-for 'script';
trusted-types appPolicy
Le code peut ensuite créer une policy autorisée :
const policy = trustedTypes.createPolicy("appPolicy", {
createHTML: input => sanitizer.sanitize(input)
});
L’intérêt est de centraliser les transformations dangereuses et de rendre les points de création de valeurs de confiance plus faciles à auditer.
Cette directive ne rend pas automatiquement le code sûr. Une policy comme :
trustedTypes.createPolicy("appPolicy", {
createHTML: input => input
});
ne sanitise rien. Elle enveloppe une chaîne arbitraire dans un objet approuvé et déplace donc la faiblesse au niveau de la policy elle-même.
Limites de Trusted Types
Trusted Types est particulièrement intéressant contre les DOM XSS liés à certains sinks, mais il ne remplace ni la validation des données ni l’analyse des comportements du code de confiance. Des scripts peuvent toujours créer des URLs, charger des ressources, interpréter des structures de données ou réaliser des opérations qui ne sont pas toutes protégées par le même mécanisme.
L’audit doit donc examiner les policies Trusted Types, les fonctions createHTML, createScript ou createScriptURL réellement définies, ainsi que les données qu’elles reçoivent. Le principal bénéfice est architectural : la surface de code à examiner est réduite et les conversions vers des types de confiance deviennent explicites.
Les limites de Content Security Policy
CSP peut considérablement réduire l’exploitabilité d’une vulnérabilité côté client, mais elle ne remplace pas les protections applicatives fondamentales.
Une injection HTML reste une vulnérabilité même si un <script> injecté est bloqué. Un attaquant peut encore modifier l’interface, créer des éléments interactifs, influencer certaines navigations, détourner un formulaire ou exploiter un comportement fourni par du JavaScript déjà autorisé.
Une application utilisant innerHTML avec des données non fiables reste également fragile, même si une CSP restrictive bloque plusieurs charges utiles évidentes. Le problème fondamental est que des données contrôlées atteignent un interpréteur HTML.
CSP considère en outre certaines ressources comme fiables par définition. Si un script autorisé contient un loader vulnérable, un gadget ou une logique qui transforme une donnée contrôlée en nouvelle ressource exécutable, le navigateur n’a aucune raison de considérer ce comportement comme hostile : l’action est réalisée par un composant auquel la politique a déjà accordé sa confiance.
Enfin, CSP n’est pas conçue pour remplacer les autres contrôles du navigateur et du serveur. HSTS, cookies sécurisés, protections CSRF, contrôles d’accès, validation des entrées, sécurité des dépendances et isolation des origines restent nécessaires. CSP doit être comprise comme une couche spécifique de défense en profondeur et non comme une politique de sécurité universelle.
Quelles sont les erreurs de configuration CSP les plus fréquentes ?
Déployer une CSP ne suffit pas à rendre une application résistante aux XSS. Une politique peut être syntaxiquement valide et correctement interprétée par le navigateur tout en construisant un modèle de confiance trop permissif. Les erreurs les plus courantes proviennent rarement d’un seul mot-clé : elles résultent souvent de compromis successifs, ajoutés pour faire fonctionner une application historique ou intégrer de nouveaux services tiers.
Dépendance excessive à unsafe-inline
La configuration suivante reste fréquente sur les applications qui utilisent de nombreux scripts inline ou gestionnaires d’évènements directement déclarés dans le HTML :
Content-Security-Policy:
script-src 'self' 'unsafe-inline'
Cette politique limite les scripts externes à l’origine courante, mais elle réautorise un mécanisme qui représente précisément une grande partie de la surface d’exploitation classique des XSS. Une injection HTML peut alors redevenir directement exécutable si elle permet d’introduire un bloc ou un attribut JavaScript compatible avec la directive effectivement appliquée.
La migration vers une CSP stricte implique généralement de déplacer les scripts inline vers des fichiers dédiés ou de les autoriser individuellement avec des nonces ou des hashes. Les event handlers inline doivent être remplacés par des listeners JavaScript lorsque cela est possible.
Il faut toutefois éviter de signaler mécaniquement toute occurrence de 'unsafe-inline' comme une vulnérabilité critique. Avec des nonces ou hashes correctement utilisés, et plus encore dans une politique CSP3 avec strict-dynamic, la valeur peut être ignorée par les navigateurs modernes tout en servant de fallback à d’anciens clients. L’analyse doit porter sur le comportement effectif.
Dépendance excessive à unsafe-eval
'unsafe-eval' est souvent introduit parce qu’un framework historique, un moteur de template, un outil de développement ou une dépendance utilise encore eval(), Function() ou une primitive similaire.
Content-Security-Policy:
script-src 'self' 'unsafe-eval'
Le problème est moins la présence du mot-clé que les opérations que le code est ensuite capable de réaliser. Si une donnée contrôlée par un utilisateur atteint une fonction d’évaluation dynamique, CSP ne bloquera plus l’exécution au titre de cette restriction.
Les équipes doivent donc traiter 'unsafe-eval' comme une exception nécessitant une justification. Une dépendance qui ne l’utilise qu’en développement ne doit pas imposer cette valeur à la politique de production. Lorsqu’un besoin WebAssembly existe sans nécessité d’évaluation JavaScript générale, 'wasm-unsafe-eval' permet un contrôle plus fin.
Wildcards et allowlists de domaines trop larges
Une politique comme :
Content-Security-Policy:
script-src 'self'
https://www.googletagmanager.com
https://cdn.jsdelivr.net
https://cdnjs.cloudflare.com
https://*.marketing.example
peut sembler raisonnable parce qu’elle n’autorise pas directement n’importe quel domaine. Pourtant, chaque source ajoutée devient une partie de la frontière de confiance de l’application.
Le problème est particulièrement important pour les domaines mutualisés, les plateformes qui hébergent du contenu utilisateur, les services capables de charger dynamiquement d’autres scripts et les sous-domaines couverts par des wildcards. Un tag manager est par nature sensible : sa fonction consiste à déclencher ou charger du code supplémentaire. L’autoriser dans script-src revient donc à déléguer une partie de la décision d’exécution à ce service et à sa configuration.
Une allowlist robuste doit être minimale, régulièrement réévaluée et construite en connaissance des fonctionnalités réellement disponibles sur chaque source, pas seulement à partir de la réputation du domaine.
Confiance excessive dans ‘self’
'self' simplifie les politiques, mais suppose que l’origine de l’application constitue une frontière de confiance cohérente.
Cette hypothèse devient fragile lorsque la même origine sert des fichiers envoyés par des utilisateurs, du contenu généré à partir d’entrées non fiables ou des ressources fournies par plusieurs applications ayant des niveaux de confiance différents.
Par exemple :
Content-Security-Policy:
script-src 'self'
ne protège pas contre une ressource contrôlée par un attaquant si cette ressource peut être servie depuis une URL telle que :
https://app.example.com/uploads/user-controlled.js
et chargée dans un contexte compatible avec JavaScript. La meilleure défense architecturale consiste à isoler les contenus utilisateurs sur une origine qui n’est pas approuvée par la politique de l’application principale.
Politique de base incomplète
Certaines politiques définissent uniquement script-src et considèrent que le reste de l’application est couvert :
Content-Security-Policy:
script-src 'self'
Cette configuration ne fournit aucun fallback aux autres directives de récupération. Une base comme :
Content-Security-Policy:
default-src 'none';
script-src 'self';
img-src 'self';
style-src 'self'
rend les intentions beaucoup plus explicites.
Il faut cependant distinguer les directives qui héritent de default-src de celles qui doivent être définies séparément. Une politique peut être très restrictive sur les ressources tout en oubliant base-uri, form-action ou frame-ancestors, laissant subsister des possibilités d’abus qui ne reposent pas nécessairement sur l’exécution de JavaScript.
Absence explicite de object-src et base-uri
Sur une application moderne, object-src 'none' et base-uri 'none' constituent souvent de bons choix par défaut lorsque les fonctionnalités correspondantes ne sont pas utilisées.
L’absence de object-src doit être interprétée avec nuance : si default-src 'none' est déjà défini, les objets restent bloqués par fallback. L’intérêt d’une directive explicite est surtout de stabiliser et documenter la décision de sécurité.
base-uri, en revanche, ne dépend pas de default-src. L’oublier peut permettre à une injection HTML d’influencer la résolution des URLs relatives si l’application contient des références concernées et si le reste de la CSP permet d’en tirer parti.
Nonces statiques, prévisibles ou mal attribués
Un nonce correctement généré peut constituer une base très robuste. Les implémentations faibles annulent cependant une grande partie de son intérêt.
Une valeur statique :
Content-Security-Policy:
script-src 'nonce-production123'
ou une valeur dérivée d’un timestamp prévisible ne possède pas les propriétés attendues d’un « number used once ».
Mais la réutilisation n’est pas le seul problème. Un nonce peut être aléatoire et renouvelé à chaque réponse, tout en étant attribué par erreur à du contenu influençable par un utilisateur. Par exemple, un moteur de rendu qui ajoute systématiquement le nonce à tous les éléments <script> d’un composant HTML contrôlable peut marquer comme fiable le script injecté par l’attaquant.
La revue doit donc couvrir la génération, le renouvellement, le stockage éventuel et surtout la logique d’injection du nonce dans le document.
CSP uniquement déployée en Report-Only
Il arrive qu’un scanner ou une revue rapide constate la présence d’un header CSP alors que celui-ci est exclusivement :
Content-Security-Policy-Report-Only: ...
Cette politique n’empêche aucune action au titre de CSP. Elle est utile pour préparer un déploiement et détecter des violations, mais ne doit pas être présentée comme une protection active lorsqu’aucune politique en enforcement n’existe.
Une autre situation fréquente consiste à disposer d’une politique stricte en Report-Only et d’une ancienne politique permissive en enforcement. Le pentester doit toujours distinguer les deux et évaluer le niveau de protection réel, pas la configuration cible envisagée par l’équipe.
Déploiement incohérent selon les routes
Une application peut envoyer une politique robuste sur /, /account ou /admin, mais oublier les pages d’erreur, les anciennes routes, les pages de support ou une sous-application produite par une autre stack.
Cette incohérence devient particulièrement intéressante si une vulnérabilité d’injection existe précisément sur l’une des pages dépourvues de politique ou protégées par une CSP différente.
L’audit d’une CSP doit donc couvrir un échantillon représentatif des routes et des contextes d’authentification. La présence d’un header correct sur la page d’accueil n’établit pas que l’ensemble de l’application partage la même protection.
Compromis de compatibilité avec les navigateurs anciens
Les politiques modernes peuvent combiner plusieurs expressions pour fournir un fallback aux navigateurs plus anciens. Une politique de migration peut par exemple contenir :
Content-Security-Policy:
script-src 'unsafe-inline' https: 'nonce-RANDOM' 'strict-dynamic'
Dans un navigateur CSP3, le modèle effectif repose sur le nonce et strict-dynamic, tandis qu’un ancien navigateur peut interpréter seulement une partie de la politique et se retrouver avec un modèle plus permissif.
Cette différence n’est pas nécessairement une vulnérabilité si les navigateurs anciens ne sont plus supportés ou si le risque a été accepté. Elle doit toutefois être comprise et documentée. Une politique doit être évaluée en fonction des clients réellement exposés et des comportements qu’ils implémentent.
Techniques courantes de contournement de CSP
Un contournement de CSP ne doit pas être confondu avec une vulnérabilité XSS. Dans la plupart des scénarios, l’attaquant dispose déjà d’une primitive : injection HTML, DOM XSS, contrôle d’un attribut, upload de fichiers ou influence sur une donnée manipulée par JavaScript. La CSP empêche l’exploitation la plus directe de cette primitive ; le contournement consiste alors à identifier un chemin qui reste compatible avec le modèle de confiance défini par la politique.
L’analyse d’un bypass doit donc répondre à quatre questions. Quelle est la primitive initiale ? Quelle opération la CSP bloque-t-elle ? Quelle ressource ou quel comportement reste considéré comme fiable ? Comment la primitive initiale permet-elle de détourner cette confiance ?
Exploitation d’une politique autorisant unsafe-inline
Considérons une application qui renvoie :
HTTP/1.1 200 OK
Content-Security-Policy:
default-src 'self';
script-src 'self' 'unsafe-inline'
Content-Type: text/html
Une fonctionnalité de recherche réinjecte le terme demandé dans la réponse sans encodage contextuel correct :
<p>Résultats pour : USER_INPUT</p>
L’auditeur constate qu’il peut introduire du HTML. Avec une politique script-src 'self' sans autre exception, un event handler inline serait normalement bloqué. Ici, la présence de ‘unsafe-inline’ peut réautoriser cette primitive.
Une payload de démonstration comme :
<img src="invalid" onerror="alert(document.domain)">
peut alors s’exécuter si script-src-attr ou un autre mécanisme plus spécifique ne l’interdit pas.
Le navigateur n’a pas été contourné au sens d’une vulnérabilité de son moteur CSP. Il applique la politique qui lui dit explicitement d’autoriser ce comportement. La faiblesse résulte du fait que la défense en profondeur censée limiter l’injection a été neutralisée par une exception trop large.
La correction consiste à supprimer la dépendance aux scripts et event handlers inline, à utiliser des nonces ou hashes pour les blocs légitimes, et surtout à corriger l’injection à sa source. Retirer 'unsafe-inline' ne rend pas un rendu HTML non encodé acceptable.
Domaine tiers de confiance et endpoint JSONP
Les CSP historiques reposent souvent sur une allowlist de domaines :
Content-Security-Policy:
default-src 'self';
script-src 'self' https://api.example.test
Une tentative de chargement direct depuis https://attacker.example/payload.js est bloquée. L’auditeur examine alors ce que api.example.test, qui est déjà autorisé, peut réellement servir.
Supposons que ce domaine expose encore un endpoint JSONP :
GET /legacy/search?callback=displayResult HTTP/1.1
Host: api.example.test
avec une réponse :
displayResult({"result":"example"});
JSONP a été conçu pour produire du JavaScript chargeable dans un élément <script>. Si le paramètre callback est insuffisamment contrôlé, l’utilisateur peut influencer la structure du code retourné.
Si l’application principale possède par ailleurs une injection HTML permettant d’introduire un élément <script src="...">, l’auditeur peut tester si l’endpoint JSONP autorisé peut être détourné pour produire du JavaScript contrôlé.
La chaîne est alors la suivante : l’injection permet d’ajouter une balise script ; la CSP refuse une origine contrôlée par l’attaquant ; le domaine JSONP est déjà approuvé ; l’endpoint de ce domaine génère du JavaScript à partir d’un paramètre utilisateur ; la réponse est donc chargée depuis une source que le navigateur considère légitime.
JSONP n’est pas intrinsèquement un bypass CSP. Le problème apparaît parce qu’une source approuvée est elle-même capable de transformer une entrée utilisateur en ressource exécutable. La remédiation consiste à supprimer les endpoints JSONP historiques lorsque cela est possible, à réduire les allowlists et à privilégier une Strict CSP basée sur nonce ou hash plutôt que sur la seule réputation des domaines.
Contournement de script-src ‘self’ via un upload
Considérons :
Content-Security-Policy:
default-src 'self';
script-src 'self'
Cette politique empêche de charger un script depuis une origine externe. L’application permet néanmoins aux utilisateurs d’envoyer des fichiers, ensuite accessibles sur :
https://app.example.test/uploads/7f94ab/file.js
Si un attaquant peut contrôler le contenu de ce fichier et que le serveur le livre dans des conditions compatibles avec un chargement actif, une injection HTML peut utiliser :
<script src="/uploads/7f94ab/file.js"></script>
La ressource appartient à la même origine que l’application. Du point de vue de script-src 'self', elle satisfait donc le modèle de confiance.
Il faut cependant éviter de présenter tout upload comme exploitable. Le serveur peut imposer un Content-Type incompatible, ajouter X-Content-Type-Options: nosniff, forcer Content-Disposition: attachment, transformer le fichier ou le servir depuis une autre origine. L’auditeur doit vérifier le comportement réel du navigateur et de la réponse.
La défense la plus robuste consiste à isoler le contenu utilisateur sur une origine qui ne figure pas dans le périmètre de confiance de l’application principale, puis à compléter cette séparation avec des types MIME corrects, nosniff, des politiques de téléchargement appropriées et une validation du contenu.
Contournement d’un nonce faible ou mal utilisé
Une politique basée sur un nonce peut sembler immédiatement robuste :
Content-Security-Policy:
script-src 'nonce-static123'
Si la même valeur est réutilisée dans toutes les réponses, un attaquant qui la connaît peut potentiellement l’employer dans une injection compatible avec le modèle de rendu de l’application.
Une autre implémentation peut générer :
const nonce = Date.now().toString();
Le problème est ici la prédictibilité de la valeur, pas la syntaxe CSP.
Un cas plus subtil apparaît lorsque le nonce est parfaitement aléatoire mais appliqué à du contenu influençable. Imaginons un composant de template qui génère automatiquement :
<script nonce="RANDOM">
loadWidget("USER_CONTROLLED_VALUE");
</script>
Si USER_CONTROLLED_VALUE est réinjectée dans un contexte JavaScript sans encodage adapté, le nonce autorise précisément le bloc qui contient la donnée dangereuse. L’attaquant n’a pas besoin de connaître ou de deviner la valeur : le serveur a déjà marqué le script vulnérable comme fiable.
Un audit des nonces doit donc vérifier leur renouvellement et leur entropie, mais également les templates et composants auxquels ils sont attribués. La remédiation consiste à générer des nonces uniques et cryptographiquement sûrs, puis à s’assurer qu’ils ne sont appliqués qu’à des scripts dont le contenu et les paramètres sont maîtrisés.
strict-dynamic et loader de script vulnérable
Une Strict CSP peut prendre la forme suivante :
Content-Security-Policy:
script-src 'nonce-K7Ew8VQvYpY5...' 'strict-dynamic';
object-src 'none';
base-uri 'none'
La page charge un script racine autorisé :
<script nonce="K7Ew8VQvYpY5..." src="/js/loader.js"></script>
Le nonce est renouvelé à chaque réponse et ne peut pas être deviné de manière réaliste. L’auditeur analyse alors ce que fait loader.js :
const params = new URLSearchParams(location.search);
const moduleUrl = params.get("module");
if (moduleUrl) {
const script = document.createElement("script");
script.src = moduleUrl;
document.head.appendChild(script);
}
Le script racine est légitime et explicitement approuvé. Avec strict-dynamic, les scripts qu’il crée programmatiquement peuvent bénéficier de la confiance propagée par cette racine. Si moduleUrl accepte une URL arbitraire contrôlée par l’utilisateur, le véritable point faible est le loader.
L’attaquant ne cherche donc pas à deviner le nonce. Il cherche à détourner un script qui possède déjà la confiance du navigateur.
Cette situation résume l’évolution du raisonnement entre une CSP par allowlist et une Strict CSP. La question n’est plus seulement « depuis quels domaines puis-je charger du JavaScript ? », mais « quels scripts sont des racines de confiance, quelles capacités possèdent-ils et quelles données peuvent influencer ces capacités ? ».
La correction ne consiste pas à retirer strict-dynamic. Il faut sécuriser le loader. Plutôt que d’accepter directement une URL, le code peut résoudre un identifiant interne vers une liste fermée de modules :
const modules = {
dashboard: "/js/dashboard.js",
profile: "/js/profile.js"
};
const moduleUrl = modules[userChoice];
if (moduleUrl) {
const script = document.createElement("script");
script.src = moduleUrl;
document.head.appendChild(script);
}
Le script reste capable de charger dynamiquement les composants nécessaires, mais l’utilisateur ne choisit plus directement la destination exécutable.
Script gadget, framework de confiance et DOM clobbering
Un bypass moderne ne nécessite pas toujours de charger un nouveau script depuis un domaine contrôlé par l’attaquant. Le code déjà autorisé peut fournir lui-même la primitive nécessaire.
Considérons un script légitime qui parcourt le DOM :
document.querySelectorAll("[data-module]").forEach(element => {
const script = document.createElement("script");
script.src = element.dataset.module;
document.head.appendChild(script);
});
Ce composant est autorisé par nonce. Parallèlement, une injection HTML permet à un utilisateur d’introduire :
<div data-module="USER_CONTROLLED_VALUE"></div>
La donnée injectée ne contient aucun JavaScript inline. C’est le code de confiance lui-même qui lit le DOM et transforme l’attribut en URL de script. Si la valeur peut pointer vers une ressource non prévue, le script devient un gadget exploitable.
Les anciennes applications AngularJS ont fourni des exemples historiques particulièrement connus de ce principe : du contenu ou des expressions injectées pouvaient être interprétés par un framework déjà autorisé, ce qui rappelait qu’une politique ne peut pas considérer tout le comportement d’un script de confiance comme automatiquement sûr.
Le DOM clobbering repose sur une idée comparable. En injectant des éléments avec certains attributs id ou name, un attaquant peut parfois influencer la résolution de propriétés que du code légitime considère comme des objets ou valeurs internes. Si cette valeur finit dans un sink sensible, une primitive HTML apparemment limitée peut devenir un élément d’une chaîne d’exploitation.
La remédiation consiste à éviter que le code de confiance transforme directement des valeurs issues du DOM en ressources ou opérations sensibles. Les références doivent être validées, résolues vers des identifiants internes et découplées autant que possible du contenu contrôlable par l’utilisateur.
Injection HTML sans exécution JavaScript, mais impact via base-uri ou form-action
Tous les impacts d’une CSP incomplète ne doivent pas être présentés comme des bypass JavaScript.
Imaginons une injection HTML sur une page protégée par une script-src robuste. Les scripts injectés sont correctement bloqués. La politique ne contient toutefois ni base-uri ni form-action.
Si l’attaquant peut injecter une balise :
<base href="https://attacker.example/">
et que la page contient ensuite des URLs relatives dont les destinations sont compatibles avec le reste de la politique, il peut influencer leur résolution. De même, l’injection ou la modification d’un formulaire peut devenir pertinente si aucune restriction form-action n’empêche une soumission vers une autre origine.
Ces scénarios ne démontrent pas nécessairement une exécution JavaScript malgré CSP. Ils montrent une autre limite importante : une injection HTML conserve des conséquences de sécurité même lorsque script-src remplit correctement son rôle. Une analyse de CSP ne doit donc pas se réduire à la question « puis-je déclencher alert(1) ? ».
DOM XSS, CSP et Trusted Types : bien distinguer les mécanismes
Une DOM XSS ne constitue pas automatiquement un contournement de CSP. Considérons :
output.innerHTML = location.hash.substring(1);
Cette instruction crée un sink dangereux. Une CSP stricte peut néanmoins empêcher certaines formes d’exécution JavaScript produites par le HTML injecté.
L’auditeur doit alors analyser ce qui se passe après l’injection : le contenu peut-il modifier une structure lue par un script de confiance ? Influencer un loader ? Introduire un attribut interprété par un framework ? Déclencher une navigation ou un formulaire ? La simple présence de innerHTML établit une surface DOM XSS, mais pas automatiquement une chaîne de bypass CSP.
Trusted Types renforce cette séparation en contrôlant directement certains sinks. Lorsque require-trusted-types-for ‘script’ est actif, une affectation de chaîne brute à innerHTML peut être rejetée. L’analyse se déplace alors vers les Trusted Types policies et la manière dont elles construisent des valeurs de confiance.
Une policy qui sanitise réellement l’entrée réduit la surface d’attaque. Une policy qui retourne la valeur telle quelle constitue au contraire un nouveau point faible. Trusted Types aide donc surtout à rendre les conversions sensibles explicites et auditables ; il ne remplace pas une logique de sanitisation correcte.
Méthodologie de pentest d’une Content Security Policy
Auditer une CSP ne consiste pas à copier sa valeur dans un outil automatisé et à rechercher quelques mots-clés considérés comme dangereux. Une analyse sérieuse cherche à reconstruire la politique réellement appliquée par le navigateur, à identifier les ressources et scripts qui constituent la frontière de confiance, puis à déterminer si une donnée contrôlée par un attaquant peut atteindre l’un de ces chemins autorisés.
Une CSP peut être perfectible sans être exploitable dans le contexte de l’application. À l’inverse, une politique qui semble très stricte à la lecture peut être contournable à cause d’un comportement spécifique du code. La méthodologie doit donc partir de la configuration brute et progresser jusqu’à une conclusion d’exploitabilité démontrée.
Collecter les politiques sur les pages représentatives
La première erreur serait d’analyser uniquement la page d’accueil. Une application peut retourner des politiques différentes selon la route, l’état d’authentification, le type de réponse ou le composant d’infrastructure ayant généré la page.
Un auditeur collecte donc les en-têtes sur les principales zones de l’application : page publique, authentification, espace utilisateur, zones administratives, pages de support, fonctionnalités d’upload, pages d’erreur et éventuelles sous-applications historiques.
Une réponse peut par exemple contenir :
GET /account HTTP/1.1
Host: app.example.test
Cookie: session=...
HTTP/1.1 200 OK
Content-Security-Policy:
default-src 'none';
script-src 'nonce-Q7jJ8...' 'strict-dynamic';
style-src 'self';
img-src 'self';
object-src 'none';
base-uri 'none';
form-action 'self'
Content-Type: text/html
La même collecte doit être répétée sur les pages produites par d’autres composants. Une route d’erreur issue d’un reverse proxy ou une vieille page servie par un backend distinct peut présenter une politique beaucoup plus permissive.
La balise <meta http-equiv="Content-Security-Policy"> doit également être recherchée lorsqu’elle est utilisée. Sa position dans le document et ses limitations doivent être prises en compte.
Distinguer les politiques en enforcement et Report-Only
L’auditeur identifie séparément Content-Security-Policy: ... et Content-Security-Policy-Report-Only: ....
Une situation fréquente consiste à trouver une politique ancienne et permissive en enforcement, tandis qu’une politique beaucoup plus stricte est testée en Report-Only :
Content-Security-Policy:
script-src 'self' 'unsafe-inline'
Content-Security-Policy-Report-Only:
script-src 'nonce-RANDOM' 'strict-dynamic';
object-src 'none';
base-uri 'none'
La seconde politique décrit probablement la cible de migration, mais elle ne protège pas encore la page. Le niveau de sécurité doit être évalué à partir des politiques réellement appliquées.
Cette distinction est également utile pour comprendre le cycle de déploiement. Une application qui reste pendant des mois avec une CSP exclusivement en Report-Only possède une instrumentation, pas une barrière active.
Reconstruire la politique effective et les fallbacks
Une politique doit être interprétée directive par directive. L’auditeur identifie les valeurs réellement utilisées pour les scripts, styles, images, connexions, workers, frames et autres capacités.
Pour un élément <script>, il faut notamment examiner script-src-elem, script-src puis default-src selon les directives présentes. Pour un event handler inline, script-src-attr peut devenir prioritaire. Pour les workers, la chaîne de fallback peut passer par worker-src, child-src, script-src puis default-src.
À l’inverse, base-uri, form-action et frame-ancestors doivent être examinées indépendamment parce qu’elles ne tombent pas automatiquement sur default-src.
Lorsque plusieurs en-têtes CSP sont présents, chaque politique en enforcement doit être satisfaite. L’auditeur ne doit donc pas fusionner les allowlists pour former une politique plus permissive que celle réellement appliquée.
Identifier le modèle de confiance de script-src
Une fois la politique comprise, l’auditeur détermine comment les scripts sont autorisés.
Une politique comme :
script-src 'self' https://cdn.example.test https://vendor.example.test
repose principalement sur une allowlist d’origines.
Une politique :
script-src 'nonce-RANDOM'
fait confiance à des scripts marqués individuellement.
Une politique :
script-src 'nonce-RANDOM' 'strict-dynamic'
construit des racines de confiance capables de propager cette confiance à certains scripts créés dynamiquement.
Enfin, une application statique peut utiliser des hashes, éventuellement combinés à strict-dynamic.
Le type de politique détermine la suite de l’audit. Dans une allowlist, l’effort se concentre sur les sources autorisées. Avec des nonces, il faut analyser la génération et l’attribution. Avec strict-dynamic, les scripts racines et leurs loaders deviennent la priorité.
Rechercher les exceptions qui modifient les possibilités d’exécution
L’auditeur examine ensuite les valeurs qui peuvent réintroduire des primitives dangereuses.
'unsafe-inline' doit être interprété avec les nonces, hashes, strict-dynamic et script-src-attr. La simple présence du mot-clé ne suffit pas à conclure.
'unsafe-eval' mérite une attention particulière parce qu’il réautorise l’évaluation de code à partir de chaînes. Il faut alors rechercher dans le JavaScript les usages de eval(), Function(), certains appels de timer construits avec une chaîne et les APIs de bibliothèques capables d’interpréter des expressions.
'wasm-unsafe-eval' doit être distingué de 'unsafe-eval' afin de comprendre si l’application a simplement besoin de compilation WebAssembly ou si elle a réactivé une surface d’évaluation beaucoup plus large.
'unsafe-hashes' peut signaler une migration qui conserve certains event handlers inline. Il faut alors examiner les handlers réellement autorisés et déterminer si leurs comportements sont influençables.
Tester la génération et l’utilisation des nonces
Lorsqu’un nonce est utilisé, l’auditeur effectue plusieurs requêtes indépendantes et compare les valeurs.
Réponse 1 :
script-src 'nonce-xF7QW2mJ0kY9...'
Réponse 2 :
script-src 'nonce-By9P1tAZ3M6c...'
Réponse 3 :
script-src 'nonce-Lh4K8dQw2NzR...'
Un comportement correct doit montrer un renouvellement à chaque réponse et des valeurs sans motif prédictible apparent. Une réutilisation stable ou une progression basée sur un compteur, un timestamp ou un identifiant court constitue un signal d’alerte.
L’analyse ne doit cependant pas s’arrêter aux valeurs. Il faut inspecter le HTML afin d’identifier les scripts qui portent le nonce. L’auditeur cherche en particulier les blocs dont le contenu ou les paramètres incluent des données utilisateur, les composants qui créent dynamiquement des scripts et les templates susceptibles de recopier le nonce sur un élément partiellement contrôlé.
Lorsqu’une revue de code est possible, la fonction de génération doit être vérifiée pour confirmer l’utilisation d’une source cryptographiquement sûre et l’absence de réutilisation entre réponses.
Analyser les hashes et les scripts qu’ils autorisent
Une CSP peut contenir :
script-src 'sha256-YWJjZGVm...' 'strict-dynamic'
L’auditeur identifie alors quel bloc ou fichier correspond à l’empreinte. L’objectif est de comprendre le rôle du script autorisé : initialise-t-il l’application, charge-t-il un bundle, crée-t-il dynamiquement d’autres scripts, lit-il des paramètres dans l’URL ou le DOM ?
Le hash garantit que le contenu approuvé correspond à une empreinte précise. Il ne garantit pas que ce contenu soit exempt de comportements détournables. Un script parfaitement authentifié par son hash peut toujours devenir un gadget s’il transforme une donnée non fiable en opération sensible.
Analyser les domaines autorisés par les allowlists
Dans une CSP basée sur des hosts, chaque source doit être traitée comme une extension de la frontière de confiance.
L’auditeur recherche sur ces domaines les fonctionnalités qui peuvent servir du contenu influençable : endpoints JSONP, espaces d’upload, CDN multi-tenant, buckets, générateurs dynamiques de JavaScript, sous-domaines de staging, services de tag management ou plateformes où un autre client peut publier une ressource.
Les wildcards exigent une cartographie plus large. Une expression https://*.example.test rend pertinents les sous-domaines oubliés, délégués ou acquis via un service externe.
L’analyse doit toutefois rester contextualisée. La présence d’un domaine tiers dans script-src n’est pas automatiquement une vulnérabilité. Il faut démontrer que ce domaine peut servir une ressource contrôlable dans une forme et à une URL qui satisfont effectivement la source CSP.
Analyser les chemins et redirections lorsqu’ils participent à la confiance
Certaines politiques essaient de réduire la confiance à un chemin :
script-src https://cdn.example.test/static/js/
L’auditeur vérifie la sémantique du matching et le comportement des redirections. CSP possède des règles spécifiques pour les chemins, et la partie chemin ne doit pas être considérée comme une frontière aussi forte qu’une origine distincte, notamment lorsqu’une redirection intervient.
Il faut également rechercher les endpoints capables de rediriger depuis un chemin autorisé vers une autre ressource. Selon le scénario et la directive, le comportement effectif du navigateur doit être validé plutôt que supposé.
Cette étape est particulièrement importante lorsque l’équipe de développement pense avoir « sécurisé » une large origine en n’autorisant qu’un sous-répertoire.
Rechercher les scripts chargés dynamiquement
Cette étape devient centrale avec strict-dynamic. L’auditeur identifie les scripts racines portant un nonce ou correspondant à un hash, puis recherche les mécanismes leur permettant de créer d’autres scripts.
Des constructions comme document.createElement("script") et script.src = value sont particulièrement importantes. La question n’est pas seulement de savoir qu’un script est créé, mais d’identifier la provenance de value.
Une logique telle que :
const module = new URLSearchParams(location.search).get("module");
const script = document.createElement("script");
script.src = module;
document.head.appendChild(script);
constitue un signal fort si aucune validation stricte n’existe.
Les loaders de modules, gestionnaires de plugins, systèmes de bundles et bibliothèques de composants dynamiques doivent être analysés de la même façon. Avec une Strict CSP, ce travail de revue du code de confiance est souvent beaucoup plus pertinent qu’une recherche classique de domaine externe autorisé.
Rechercher les script gadgets et les flux de données contrôlables
Au-delà des loaders évidents, le pentester recherche les comportements où du code de confiance lit une donnée depuis l’environnement puis l’utilise dans une opération sensible.
Les sources courantes incluent location.search, location.hash, postMessage, localStorage, sessionStorage, des attributs data-*, des champs du DOM ou des réponses d’API réinjectées côté client.
Une donnée n’est pas dangereuse simplement parce qu’elle vient de location.hash. Elle devient intéressante lorsqu’elle atteint un sink : création d’un script, génération de HTML, navigation, sélection d’un template, appel de fonction dynamique ou autre primitive capable de produire un effet de sécurité.
Ce suivi consiste à partir de la source, à examiner les transformations successives, puis à identifier le sink atteint. Il peut être présenté entièrement sous forme textuelle : la donnée est lue depuis le fragment de l’URL, recopiée dans un attribut du DOM, puis consommée par un script autorisé qui utilise cet attribut comme URL de module.
Cartographier les sinks DOM
Les opérations capables d’interpréter du contenu contrôlé doivent être recherchées, notamment :
element.innerHTML = value;
element.outerHTML = value;
element.insertAdjacentHTML("beforeend", value);
document.write(value);
Il faut également examiner les APIs de framework équivalentes, comme les mécanismes permettant explicitement d’injecter du HTML brut.
L’objectif n’est pas de qualifier automatiquement chaque sink de bypass. Une affectation à innerHTML peut créer du HTML sans que le JavaScript injecté s’exécute sous une CSP stricte. Le pentester doit poursuivre l’analyse : le contenu peut-il influencer un script gadget, modifier un attribut lu par un loader, créer une navigation utile ou atteindre une autre capacité autorisée ?
Cette distinction évite de confondre une DOM XSS potentielle, une injection HTML et un bypass CSP effectivement démontré.
Analyser Trusted Types lorsqu’il est activé
Si la politique contient :
require-trusted-types-for 'script'
l’auditeur recherche les appels à :
trustedTypes.createPolicy(...)
et examine les fonctions de création réellement utilisées, comme createHTML, createScript ou createScriptURL.
Une policy qui passe les données dans un sanitizer correctement configuré peut réduire significativement la surface. Une policy qui retourne simplement l’entrée :
trustedTypes.createPolicy("appPolicy", {
createHTML: input => input
});
ne fournit pas la même protection.
La directive trusted-types doit également être examinée pour savoir quelles policies peuvent être créées. Plus l’application limite ces points de création, plus la revue est simple et plus il devient difficile pour une dépendance de créer une policy permissive de manière inattendue.
Analyser les capacités autres que l’exécution JavaScript
Une CSP peut empêcher toute exécution JavaScript injectée et laisser pourtant subsister un impact intéressant.
L’auditeur examine donc base-uri, form-action, frame-ancestors, img-src, connect-src, frame-src et les autres directives pertinentes selon la fonctionnalité.
Une injection HTML peut par exemple modifier un formulaire même si les scripts sont bloqués. Si form-action est absent, il faut vérifier si une soumission vers une origine externe est possible. Si base-uri est absent, l’auditeur peut tester l’impact d’une balise <base> sur les URLs relatives. Si frame-ancestors n’est pas défini, la résistance au clickjacking doit être évaluée séparément.
Cette approche donne une vision plus juste de l’impact d’une injection que le simple test d’un alert(1).
Utiliser les DevTools pour valider les hypothèses
Les outils de développement du navigateur sont essentiels pour observer la CSP effective. La console indique généralement quelle directive a bloqué une opération et le panneau Network permet de confirmer les headers reçus, les redirections, les ressources effectivement chargées et les réponses des endpoints de test.
Lorsqu’un payload est bloqué, le message du navigateur permet souvent de distinguer un refus par script-src-elem, script-src-attr, connect-src ou une autre directive. Ces informations sont particulièrement utiles lorsque plusieurs politiques ou fallbacks rendent la lecture de l’en-tête moins intuitive.
L’absence de message d’erreur ne prouve cependant pas qu’une politique est sûre. Elle signifie seulement que le comportement testé n’a pas provoqué de violation observable. L’audit doit rester guidé par le modèle de confiance et le code de l’application.
Utiliser CSP Evaluator
CSP Evaluator peut accélérer l’identification de certaines configurations connues pour être faibles, signaler des allowlists dangereuses et donner une première lecture d’une politique.
Il ne connaît toutefois pas tous les domaines, tous les endpoints ni les comportements spécifiques du code applicatif. Une politique peut ne générer aucun avertissement majeur tout en contenant un loader vulnérable. Inversement, une configuration signalée comme perfectible peut ne pas conduire à une exploitation dans le contexte réel.
L’outil doit donc être utilisé comme un accélérateur de revue et non comme un substitut à l’analyse manuelle.
Vérifier le reporting CSP
Lorsque le reporting est activé, l’auditeur vérifie que l’endpoint est correctement déclaré et que les rapports sont effectivement reçus.
Une configuration moderne peut ressembler à :
Reporting-Endpoints:
csp-endpoint="https://reports.example.test/csp"
Content-Security-Policy:
default-src 'self';
report-to csp-endpoint
Selon les besoins de compatibilité, report-uri peut être conservé temporairement en parallèle.
L’endpoint de collecte doit être protégé contre les abus. Les rapports sont des données reçues de clients et certaines propriétés peuvent contenir des valeurs contrôlées par un attaquant. Les systèmes qui les affichent ou les indexent doivent appliquer les mêmes principes de validation, encodage et limitation de volumétrie que n’importe quelle autre entrée non fiable.
Comment construire une CSP robuste ?
Une bonne CSP ne doit pas être conçue comme une liste de domaines accumulés au fil des besoins. Elle doit partir des capacités réellement nécessaires à l’application et réduire autant que possible la confiance implicite.
Pour les applications modernes, la stratégie la plus robuste consiste généralement à utiliser une politique stricte pour JavaScript, basée sur des nonces ou des hashes, puis à définir explicitement les autres catégories de ressources. Cette approche n’est pas toujours immédiatement applicable à une application historique, mais elle constitue une cible de migration plus saine qu’une allowlist qui s’allonge indéfiniment.
Commencer par les besoins fonctionnels réels
Avant d’écrire la politique, l’équipe doit identifier les ressources réellement utilisées par l’application : scripts applicatifs, APIs, images, polices, workers, frames, médias, formulaires et intégrations externes.
La logique de moindre privilège consiste ensuite à refuser ce qui n’est pas nécessaire puis à réautoriser les capacités utiles. Un point de départ peut être :
Content-Security-Policy:
default-src 'none'
Cette valeur ne constitue pas à elle seule une politique complète. Elle oblige simplement l’équipe à expliciter les ressources nécessaires au lieu de dépendre d’autorisations implicites.
Les directives qui ne possèdent pas de fallback vers default-src, notamment base-uri, form-action et frame-ancestors, doivent être ajoutées séparément en fonction du comportement attendu.
Privilégier les nonces ou hashes pour JavaScript
Une allowlist historique peut prendre la forme :
script-src 'self'
https://cdn1.example
https://cdn2.example
https://vendor.example
Ce modèle délègue la confiance à plusieurs origines entières. Une politique basée sur un nonce :
script-src 'nonce-RANDOM'
autorise au contraire les scripts marqués par l’application pour la réponse courante.
Lorsque des scripts autorisés doivent charger dynamiquement d’autres scripts, strict-dynamic peut être utilisé :
script-src 'nonce-RANDOM' 'strict-dynamic'
Cette approche réduit la dépendance aux domaines tiers, mais impose de traiter les scripts racines comme des composants de sécurité. Leur code doit être revu avec attention, notamment lorsqu’ils construisent dynamiquement des URLs ou des éléments <script>.
Pour les applications statiques, des hashes peuvent constituer une alternative pertinente lorsque les scripts inline changent rarement.
Interdire explicitement les event handlers inline lorsque cela est possible
Une politique moderne peut compléter script-src avec :
script-src-attr 'none'
Cette directive interdit les gestionnaires d’évènements inline et facilite une séparation claire entre structure HTML et logique JavaScript.
Le code :
<button onclick="save()">Enregistrer</button>
peut être remplacé par :
button.addEventListener("click", save);
Cette migration simplifie également l’usage des nonces et réduit le besoin de valeurs de compatibilité comme 'unsafe-inline' ou 'unsafe-hashes'.
Supprimer progressivement unsafe-inline et unsafe-eval
Les exceptions historiques doivent faire l’objet d’un plan de réduction plutôt que devenir permanentes par inertie.
Les scripts inline légitimes peuvent être déplacés, associés à un nonce ou couverts par un hash. Les event handlers inline peuvent être remplacés par des listeners. Les dépendances exigeant eval() doivent être identifiées afin de déterminer si cette exigence est réellement nécessaire en production.
Le retrait de 'unsafe-eval' peut demander des changements de build ou de framework. Il est souvent plus réaliste de le traiter comme un chantier de migration que de bloquer brutalement l’application. L’objectif reste toutefois de réduire progressivement le nombre de mécanismes capables de transformer une chaîne en code exécutable.
Minimiser les domaines tiers
Chaque domaine présent dans une directive sensible doit pouvoir être justifié.
Pour script-src, cette exigence est particulièrement forte. L’équipe doit comprendre si le domaine est dédié à l’organisation, s’il est mutualisé, si des utilisateurs peuvent y publier des fichiers, s’il charge lui-même des scripts additionnels et si l’intégration est nécessaire sur toutes les pages.
Les services de tag management, A/B testing, analytics avancés et widgets externes doivent être traités comme des composants de la surface d’attaque. Lorsqu’ils sont nécessaires, leur périmètre peut parfois être limité à certaines pages ou isolé dans des contextes moins privilégiés.
L’objectif n’est pas d’interdire tout tiers, mais de ne jamais confondre « fournisseur connu » et « source CSP sûre par définition ».
Isoler les contenus contrôlés par les utilisateurs
Les fichiers envoyés par des utilisateurs ne devraient idéalement pas partager l’origine depuis laquelle l’application charge ses scripts de confiance.
Une architecture dédiée, par exemple :
https://uploads.example-cdn.test/
permet de sortir ces ressources du périmètre couvert par 'self' de l’application principale.
Cette séparation doit être complétée par des types MIME cohérents, X-Content-Type-Options: nosniff, des politiques de téléchargement adaptées, des restrictions de contenu et, lorsque nécessaire, des contrôles antivirus ou de transformation.
L’isolation d’origine est une mesure particulièrement robuste car elle simplifie le modèle de confiance : le navigateur peut continuer à considérer l’origine applicative comme sensible sans inclure automatiquement tous les contenus utilisateurs.
Utiliser base-uri, form-action, frame-ancestors et object-src explicitement
Une politique moderne ne doit pas se focaliser exclusivement sur JavaScript.
Lorsque l’application n’utilise pas <base> : base-uri 'none' est un choix simple.
Lorsque les formulaires ne doivent être envoyés que vers l’application : form-action 'self' réduit les destinations possibles.
Si aucun framing n’est requis : frame-ancestors 'none' limite le clickjacking.
Enfin, pour les applications modernes qui n’utilisent pas les contenus contrôlés par <object> ou <embed> : object-src 'none' supprime explicitement cette surface.
Ces directives rendent la politique plus lisible et empêchent des capacités non nécessaires de réapparaître au gré d’un futur changement de default-src.
Adopter Trusted Types lorsque l’architecture le permet
Trusted Types peut compléter une Strict CSP en réduisant les affectations directes de chaînes vers certains sinks DOM sensibles.
Une stratégie de migration peut commencer en observation afin d’identifier les opérations incompatibles, puis activer progressivement :
require-trusted-types-for 'script';
trusted-types appPolicy
Les opérations légitimes sont regroupées derrière des policies auditées qui sanitise ou valident réellement les entrées avant de produire des objets de confiance.
Il faut résister à la tentation de créer une « default policy » permissive qui accepterait tout uniquement pour faire disparaître les erreurs. Une policy qui transforme aveuglément une chaîne en TrustedHTML ou TrustedScriptURL réduit fortement le bénéfice du mécanisme.
Trusted Types est particulièrement utile dans les applications riches où la suppression immédiate de tous les sinks DOM dangereux est difficile. Il permet de concentrer la revue sur un nombre réduit de conversions explicites.
Déployer la politique progressivement avec Report-Only
Une CSP stricte introduite brutalement sur une application existante risque de bloquer des fonctionnalités légitimes. Le mode Report-Only permet de préparer la migration sans casser la production.
Une équipe peut commencer par :
Content-Security-Policy-Report-Only:
default-src 'none';
script-src 'nonce-RANDOM' 'strict-dynamic';
object-src 'none';
base-uri 'none';
report-to csp-endpoint
Les violations observées permettent d’identifier les dépendances oubliées et les portions de code qui nécessitent une adaptation.
La politique doit ensuite être durcie puis transférée vers Content-Security-Policy une fois que son comportement est suffisamment maîtrisé. Une erreur fréquente consiste à rester indéfiniment en Report-Only : à ce stade, CSP produit des données mais ne remplit pas son rôle d’enforcement.
Surveiller les violations sans transformer le reporting en bruit
Les rapports CSP peuvent révéler des ressources inattendues, des régressions, des intégrations nouvelles, des extensions de navigateur qui injectent du contenu ou certaines tentatives d’exploitation.
Ils génèrent toutefois aussi du bruit. Une infrastructure de reporting doit donc agréger, dédupliquer et prioriser les événements plutôt que produire une alerte pour chaque violation individuelle.
Les tendances sont souvent plus utiles que les événements isolés : apparition d’un nouveau domaine bloqué après un déploiement, hausse soudaine de violations sur une directive, répétition d’une même ressource sur plusieurs pages ou comportement différent selon un navigateur.
Les données reçues doivent être considérées comme non fiables et traitées en conséquence dans l’interface de supervision.
Maintenir la politique dans le temps
Une CSP efficace au jour de son déploiement peut devenir permissive quelques mois plus tard si chaque nouvelle intégration ajoute une exception sans retrait des anciennes.
La politique doit donc faire partie du cycle de vie de l’application. Les revues périodiques doivent vérifier les domaines tiers encore nécessaires, les nonces et hashes, les violations récurrentes, les directives devenues obsolètes ou inutilisées, ainsi que les évolutions du code de confiance.
Les modifications importantes du frontend, les migrations de framework, les nouveaux loaders, les changements de CDN et les nouvelles fonctionnalités d’upload doivent déclencher une revue du modèle de confiance CSP.
Le même principe s’applique aux navigateurs supportés. Les fonctionnalités comme Trusted Types ou certaines directives CSP3 évoluent en termes de compatibilité ; la politique doit être conçue en connaissance du parc client réel et de la stratégie de rétrocompatibilité retenue.
Conclusion
Content Security Policy constitue l’un des mécanismes de défense en profondeur les plus puissants disponibles côté navigateur, mais son efficacité dépend entièrement de la manière dont la confiance est définie.
Une politique longue n’est pas nécessairement une politique robuste. Une CSP reposant sur une allowlist volumineuse peut accorder sa confiance à de nombreux domaines qu’une équipe ne maîtrise pas complètement. À l’inverse, une politique relativement courte basée sur des nonces correctement générés, des hashes ou strict-dynamic peut réduire considérablement la surface d’exécution.
Pour un attaquant ou un auditeur, analyser une CSP consiste donc moins à rechercher un mot-clé « mauvais » qu’à reconstruire l’ensemble du modèle de confiance. Quels scripts sont autorisés ? Pourquoi le sont-ils ? Peuvent-ils charger d’autres scripts ? Quelles données contrôlables influencent ces loaders ? Quels domaines sont considérés comme fiables ? Ces domaines peuvent-ils héberger du contenu utilisateur ? Les nonces sont-ils correctement générés et attribués ? Des scripts gadgets permettent-ils de transformer une injection HTML limitée en opération exécutable ? Les formulaires, frames, workers, uploads et URLs de base sont-ils eux aussi correctement encadrés ?
C’est en répondant à ces questions qu’il devient possible de déterminer si une CSP apporte une véritable barrière de sécurité ou si elle ne fait que compliquer légèrement l’exploitation d’une vulnérabilité existante.
Du côté défensif, l’objectif doit être de réduire la confiance implicite. Cela passe par l’adoption progressive d’une Strict CSP, la maîtrise des nonces et hashes, l’utilisation correcte de strict-dynamic, la réduction des tiers, l’isolation des contenus non fiables, la suppression des exceptions historiques et, lorsque cela est compatible avec l’application, l’utilisation de Trusted Types pour mieux contrôler les sinks DOM sensibles.
CSP ne remplace ni un développement sécurisé ni la prévention des XSS à leur origine. Correctement conçue, elle constitue cependant une couche supplémentaire capable de transformer de nombreux scénarios client-side potentiellement critiques en chaînes d’exploitation nettement plus difficiles, voire impossibles à réaliser dans les conditions prévues.
Références
- W3C, Content Security Policy Level 3 : https://www.w3.org/TR/CSP3/
- MDN Web Docs, Content Security Policy (CSP) : https://developer.mozilla.org/fr/docs/Web/HTTP/Guides/CSP
- MDN Web Docs, référence de l’en-tête Content-Security-Policy : https://developer.mozilla.org/fr/docs/Web/HTTP/Reference/Headers/Content-Security-Policy