Skip to Content

Chapitre 7.4 — CSP & Trusted Types

⏱️ TL;DR — La CSP (Content-Security-Policy) est le filet anti-XSS : elle dit au navigateur quelles sources de scripts sont autorisées, et bloque tout le reste — y compris le JavaScript inline injecté par un attaquant. Bien faite (pas de 'unsafe-inline', scripts autorisés par nonce ou hash), elle empêche l’exécution d’un XSS même si l’encodage a été oublié quelque part. Elle ne remplace pas l’encodage/la sanitization — c’est une couche de plus (defense in depth). Les Trusted Types vont plus loin côté DOM-XSS : ils imposent que toute écriture dans un sink dangereux (innerHTML…) passe par une fonction de nettoyage, transformant une classe entière de bugs en erreurs bloquantes.

🎯 Objectifs

  • Comprendre comment une CSP bloque l’exécution d’un XSS et ses limites.
  • Écrire une CSP stricte (nonces/hashes) plutôt qu’une CSP trouée ('unsafe-inline').
  • Déployer une CSP progressivement (Report-Only, rapports de violation).
  • Comprendre les Trusted Types comme défense structurelle contre le DOM-XSS.

La CSP comme filet anti-XSS

On a vu la CSP côté en-têtes (chapitre 2.4). Ici, on se concentre sur son rôle anti-XSS. L’idée : même si un attaquant réussit à injecter du <script> ou un onerror= dans ta page (encodage oublié, sanitization contournée), le navigateur refusera de l’exécuter s’il ne correspond pas à ce que ta CSP autorise.

Une CSP stricte pour les scripts ressemble à :

Content-Security-Policy: script-src 'self' 'nonce-r4nd0m'; object-src 'none'; base-uri 'self'
  • script-src 'self' : n’exécuter que les scripts servis depuis ta propre origine.
  • 'nonce-r4nd0m' : autoriser précisément les scripts inline portant ce nonce (un jeton aléatoire régénéré à chaque réponse).
  • Ce qui n’est pas listé est bloqué : un <script> injecté par XSS n’a pas le bon nonce → non exécuté. Un onerror= inline injecté → bloqué (l’inline non autorisé est refusé).

C’est là toute la force : une CSP stricte interdit par défaut le JavaScript inline non prévu — or c’est exactement ce qu’un XSS injecte. L’attaque peut « réussir » à insérer le HTML, mais elle échoue à l’exécuter.

Le nonce : autoriser TES scripts sans ouvrir la porte

Le problème : ta page a aussi des scripts inline légitimes (un bout d’init, des données). Comment les autoriser sans réautoriser tout l’inline (ce que ferait 'unsafe-inline', qui casse la protection) ? Réponse : le nonce. Le serveur génère à chaque réponse un jeton aléatoire (CSPRNG — Partie 3), le met dans l’en-tête CSP et sur chaque <script> légitime. Le navigateur n’exécute que les scripts portant le bon nonce.

<!-- Le serveur pose le meme nonce dans la CSP et sur ses scripts legitimes --> <script nonce="r4nd0m">initApp()</script> <!-- Un script injecte par XSS n'a PAS ce nonce (imprevisible) -> bloque -->

Comme le nonce est imprévisible et change à chaque requête, l’attaquant ne peut pas le deviner pour le mettre sur son script injecté. Alternative au nonce : le hash (autoriser un script inline par l’empreinte de son contenu), pratique pour des scripts statiques.

⚠️ Piège — Une CSP avec script-src 'unsafe-inline' ne protège quasiment pas du XSS : 'unsafe-inline' réautorise précisément le JavaScript inline que l’attaquant injecte. C’est l’erreur la plus commune — on met une CSP « pour cocher la case », mais avec 'unsafe-inline' (souvent ajouté pour « faire marcher » des scripts existants), le filet est troué. Utilise des nonces ou hashes pour tes scripts légitimes, et retire 'unsafe-inline'. Même prudence avec 'unsafe-eval' (réautorise eval, un sink dangereux).

Déployer une CSP sans casser le site

Une CSP stricte casse souvent des choses au début (scripts tiers, inline non noncé, styles). On la déploie donc progressivement :

  1. Content-Security-Policy-Report-Only : la politique est évaluée et les violations sont signalées (via report-to/report-uri), mais rien n’est bloqué. On observe ce qui serait cassé, en production, sans impact utilisateur.
  2. On corrige : ajouter les nonces sur les scripts légitimes, whitelister les sources tierces réellement nécessaires, éliminer l’inline non noncé.
  3. Quand les rapports sont propres, on bascule en Content-Security-Policy (mode bloquant).
  4. On surveille les rapports en continu (une salve de violations script-src peut signaler une tentative de XSS en cours — lien avec la Partie 15).
# Etape 1 : on observe sans bloquer Content-Security-Policy-Report-Only: script-src 'self' 'nonce-r4nd0m'; report-to csp-endpoint

📚 La source — Rédiger une CSP stricte et robuste est un art (interactions avec les frameworks, scripts tiers, styles). Les références à jour : MDN (directives), l’OWASP Content Security Policy Cheat Sheet, et l’évaluateur CSP Evaluator de Google (qui détecte les CSP trouées). Beaucoup de frameworks (dont Next.js) ont un support des nonces — voir Partie 10. On enseigne ici le principe (strict, nonce, pas d’unsafe-inline, déploiement en Report-Only) ; les directives fines s’affinent avec ces sources.

Trusted Types : verrouiller les sinks DOM

Le DOM-XSS (chapitre 7.1) vient de données non fiables écrites dans des sinks dangereux (innerHTML, document.write, eval…). Les Trusted Types sont un mécanisme (activable via CSP) qui s’attaque à ça structurellement : quand ils sont activés, le navigateur refuse qu’une chaîne brute soit affectée à un sink dangereux — seule une valeur produite par une fonction de nettoyage déclarée (une policy Trusted Types) est acceptée.

Content-Security-Policy: require-trusted-types-for 'script'
// Avec Trusted Types actifs : element.innerHTML = donneeUtilisateur // ❌ ERREUR : chaine brute refusee au sink // Il FAUT passer par une policy qui nettoie (ex. avec DOMPurify) : const policy = trustedTypes.createPolicy('sanitize', { createHTML: s => DOMPurify.sanitize(s) }) element.innerHTML = policy.createHTML(donneeUtilisateur) // ✅ valeur "de confiance"

L’effet est puissant : une classe entière de bugs (affecter une chaîne non nettoyée à innerHTML) devient une erreur bloquante au lieu d’une faille silencieuse. On ne peut plus oublier de nettoyer, car le navigateur l’impose. C’est du secure by default poussé au niveau du DOM. Support et déploiement se font progressivement (comme la CSP), mais c’est une direction forte pour les applis à fort enjeu.

💡 Réflexe — Hiérarchie des défenses XSS, à empiler : (1) auto-échappement du framework pour le texte (couche principale, gratuite) ; (2) sanitization (DOMPurify) pour le HTML riche autorisé ; (3) CSP stricte (nonces, pas d’unsafe-inline) comme filet d’exécution ; (4) Trusted Types pour verrouiller les sinks DOM. Aucune seule ne suffit ; ensemble, elles rendent le XSS très difficile à écrire et à exécuter. La CSP et les Trusted Types rattrapent précisément les oublis des couches 1 et 2.

🧭 Sur FormaCampus — FormaCampus déploie une CSP stricte : script-src 'self' + nonce par réponse (généré côté serveur, posé sur les scripts légitimes), pas d’unsafe-inline ni d’unsafe-eval, object-src 'none', base-uri 'self', frame-ancestors 'none'. Elle a été déployée d’abord en Report-Only pendant plusieurs semaines pour nettoyer les violations sans impact, puis passée en mode bloquant ; les rapports de violation sont monitorés (une salve script-src déclenche une alerte de possible XSS). Les Trusted Types sont en cours d’activation sur le front, avec une policy branchée sur DOMPurify pour le contenu riche des cours. Résultat : même un encodage oublié verrait le script injecté bloqué à l’exécution.

✏️ Exercices

Exercice 1 — Répare la CSP. Cette CSP est censée protéger du XSS mais ne le fait quasiment pas : Content-Security-Policy: script-src 'self' 'unsafe-inline'. Explique et corrige.

✅ Solution

'unsafe-inline' réautorise tout le JavaScript inline, y compris celui qu’un attaquant injecte (<script>...</script>, onerror=...) : la CSP ne bloque donc pas le XSS, alors que c’est son but principal. Correctif : retirer 'unsafe-inline' et autoriser les scripts inline légitimes par un nonce (script-src 'self' 'nonce-XXXX', le nonce étant aléatoire, régénéré à chaque réponse et posé sur chaque <script> de confiance) ou par hash. Un script injecté n’aura pas le bon nonce → bloqué. Ajouter aussi object-src 'none' et base-uri 'self' pour durcir. Déployer en Report-Only d’abord pour ne rien casser.

Exercice 2 — Que fait ce filet ? Ton encodage de sortie a un trou : un attaquant réussit à insérer <script>vol()</script> dans une page. Comment une CSP stricte (avec nonce) change-t-elle l’issue, et quelle est sa limite ?

✅ Solution

Avec une CSP stricte (script-src 'self' 'nonce-...', sans 'unsafe-inline'), le <script>vol()</script> injecté n’a pas le bon nonce → le navigateur refuse de l’exécuter. L’attaque a réussi à insérer le HTML mais échoue à l’exécution : le XSS est neutralisé malgré le trou d’encodage. C’est le rôle de filet (defense in depth). Limite : la CSP ne corrige pas le bug d’encodage (il faut quand même le réparer), elle ne protège pas contre tout (ex. exfiltration via des sources autorisées, certains vecteurs), et une CSP mal écrite (avec 'unsafe-inline') n’offre aucune protection. La CSP complète l’encodage/la sanitization, elle ne les remplace pas.

🧠 Quiz de révision

1. Comment une CSP stricte bloque-t-elle un XSS ?

En n’autorisant que des sources de scripts précises et en interdisant le JavaScript inline non prévu : un <script> ou onerror= injecté par l’attaquant n’a pas la bonne autorisation (nonce/hash) et le navigateur refuse de l’exécuter. L’injection peut réussir, mais pas l’exécution.

2. Qu’est-ce qu’un nonce et pourquoi est-il efficace ?

Un jeton aléatoire (CSPRNG), régénéré à chaque réponse, mis dans la CSP et sur chaque <script> légitime. Le navigateur n’exécute que les scripts portant ce nonce. Comme il est imprévisible et change à chaque requête, l’attaquant ne peut pas le mettre sur son script injecté → celui-ci est bloqué. Il autorise tes scripts sans réautoriser tout l’inline.

3. Pourquoi 'unsafe-inline' ruine-t-il la protection ?

Parce qu’il réautorise tout le JavaScript inline — exactement ce qu’un XSS injecte. Une CSP avec 'unsafe-inline' sur script-src ne bloque plus le XSS. On l’évite absolument, en utilisant nonces/hashes pour les scripts légitimes.

4. Pourquoi déployer une CSP en Report-Only d’abord ?

Parce qu’une CSP stricte casse souvent des choses (scripts tiers, inline non noncé) : le mode Report-Only évalue et signale les violations sans bloquer, permettant d’observer et corriger en production sans impact utilisateur, avant de basculer en mode bloquant. Les rapports servent ensuite à détecter des tentatives de XSS.

5. Qu’apportent les Trusted Types contre le DOM-XSS ?

Ils imposent que toute écriture dans un sink dangereux (innerHTML, etc.) passe par une fonction de nettoyage déclarée ; une chaîne brute est refusée par le navigateur. Une classe entière de bugs (chaîne non nettoyée → sink) devient une erreur bloquante au lieu d’une faille silencieuse — on ne peut plus oublier de nettoyer.


Fin de la Partie 7. Le XSS est cerné : trois types compris, encodage contextuel et sanitization en défense principale, frameworks maîtrisés, CSP et Trusted Types en filet. On passe aux attaques qui détournent des requêtes — au nom de la victime ou du serveur : la Partie 8 — CSRF, SSRF & CORS.

Last updated on