Chapitre 7.2 — Encodage & sanitization
⏱️ TL;DR — La parade centrale au XSS, c’est l’encodage de sortie : au moment d’insérer une donnée dans la page, on la transforme pour qu’elle soit affichée comme du texte, pas interprétée comme du code. Mais l’encodage est contextuel : encoder pour du HTML (
<→<), pour un attribut, pour du JavaScript ou pour une URL sont des opérations différentes — se tromper de contexte laisse passer le XSS. Quand tu dois vraiment autoriser du HTML riche (contenu de cours, commentaires formatés), l’encodage ne convient pas : il faut sanitizer (nettoyer) le HTML avec une bibliothèque éprouvée comme DOMPurify, qui ne garde que les balises/attributs sûrs.
🎯 Objectifs
- Comprendre l’encodage de sortie et pourquoi il neutralise le XSS.
- Choisir le bon encodage selon le contexte (HTML, attribut, JS, URL).
- Distinguer les cas « afficher du texte » (encoder) et « autoriser du HTML » (sanitizer).
- Utiliser DOMPurify pour nettoyer du HTML riche, et éviter les pièges de la sanitization.
Encoder la sortie : afficher comme texte, pas comme code
La règle d’or contre le XSS : valide en entrée, encode en sortie. L’encodage de sortie (ou échappement) intervient au moment de l’affichage : il remplace les caractères qui ont une signification dans le contexte cible par leur équivalent neutre, pour que la donnée soit rendue comme du texte visible et non exécutée.
En contexte HTML, cela signifie transformer les caractères spéciaux :
< -> <
> -> >
& -> &
" -> "
' -> 'Ainsi, si un utilisateur a pour pseudo <script>alert(1)</script>, l’encodage HTML l’affiche littéralement à l’écran (les < et > sont montrés) au lieu de créer une vraie balise <script> exécutée :
// ✅ PHP : htmlspecialchars encode pour le contexte HTML
echo 'Bonjour ' . htmlspecialchars($nom, ENT_QUOTES, 'UTF-8');
// <script> devient <script> -> affiche comme texte, pas executeC’est le même principe que le paramétrage SQL : on empêche la donnée de changer la structure (ici le HTML), en la confinant au rôle de contenu.
L’encodage est CONTEXTUEL
Le piège majeur : il n’existe pas un encodage universel. La transformation correcte dépend de où la donnée est insérée. Une donnée encodée pour du HTML peut rester dangereuse dans un attribut, dans du JavaScript ou dans une URL.
| Contexte d’insertion | Exemple | Encodage requis |
|---|---|---|
| Corps HTML | <p>DONNEE</p> | Encodage HTML (<, >, &…) |
| Attribut HTML | <input value="DONNEE"> | Encodage d’attribut (guillemets + HTML) ; toujours guillemeter |
| JavaScript | <script>var x = "DONNEE"</script> | Encodage JS (échapper ", ', \, </) — à éviter, préférer passer par des data-attributes / JSON |
| URL / paramètre | <a href="/x?q=DONNEE"> | Encodage URL (encodeURIComponent) + valider le schéma |
| CSS | style="width: DONNEE" | Encodage CSS — à éviter |
# Meme donnee " onmouseover=alert(1) x=" inseree dans un attribut NON guillemete :
<input value=DONNEE> -> <input value= onmouseover=alert(1) x=> # XSS !
# Il fallait guillemeter ET encoder l'attribut.⚠️ Piège — Insérer une donnée dans un contexte JavaScript (
<script>var nom = "…"</script>) est particulièrement casse-gueule : une séquence comme</script>dans la donnée ferme la balise et rouvre l’injection, et l’encodage JS a des subtilités. Évite ce contexte : ne construis pas de JS avec des données serveur par concaténation. Passe les données du serveur au client via undata-*attribute encodé, ou un<script type="application/json">contenant du JSON encodé, que ton JS lit ensuite proprement. « Générer du JS avec des variables » est un anti-pattern.
💡 Réflexe — Ne fais pas l’encodage à la main caractère par caractère : utilise l’auto-échappement de ton moteur de rendu (React, Twig — chapitre 7.3) ou la fonction d’encodage du bon contexte de ta plateforme (
htmlspecialcharspour HTML,encodeURIComponentpour URL). Le principe à retenir : encoder au dernier moment, pour le contexte exact d’insertion. Encoder trop tôt (à l’entrée, « une fois pour toutes ») est une erreur : la même donnée peut aller dans plusieurs contextes, et le double-encodage corrompt l’affichage.
Quand il faut autoriser du HTML : sanitizer
L’encodage résout le cas « je veux afficher du texte ». Mais parfois, on veut légitimement autoriser du HTML riche : un éditeur WYSIWYG pour le contenu d’un cours, des commentaires avec gras/liens/listes, une description formatée. Là, encoder tout casserait la mise en forme (on afficherait les balises au lieu de les appliquer). Il faut une autre approche : la sanitization.
Sanitizer, c’est analyser le HTML fourni et n’en garder que les éléments sûrs — une allow-list de balises (<b>, <i>, <a>, <ul>, <p>…) et d’attributs (href limité aux schémas sûrs), en supprimant tout le reste (<script>, onerror=, javascript:, <iframe>, etc.). On ne fait jamais ça soi-même avec des regex (contournable de mille façons) : on utilise une bibliothèque éprouvée et maintenue.
Côté navigateur, la référence est DOMPurify :
import DOMPurify from 'dompurify'
// ✅ Nettoie le HTML riche : garde le formatage sur, retire tout script/handler
const propre = DOMPurify.sanitize(htmlUtilisateur)
element.innerHTML = propre
// <b>Titre</b> reste ; <img src=x onerror=vol()> et <script> sont retiresPoints de vigilance sur la sanitization :
- Où sanitizer ? Idéalement au rendu (juste avant l’affichage), ou de façon cohérente. Sanitizer côté serveur et faire confiance au client, ou l’inverse, demande de la rigueur : le plus sûr est de sanitizer et de re-rendre dans un contexte contrôlé. Beaucoup d’équipes sanitizent à l’entrée (stockage) et comptent sur l’auto-échappement au rendu — mais attention, si on stocke du HTML « propre », il faut le rendre en tant que HTML (donc la sanitization est la seule barrière : elle doit être parfaite et à jour).
- Configurer l’allow-list selon le besoin réel (pas plus de balises que nécessaire).
- Attention aux attributs :
href/srcpeuvent porterjavascript:— DOMPurify les gère, mais si tu configures, ne réautorise pas de schémas dangereux. - Garder la lib à jour : des contournements (mutation XSS) sont découverts et corrigés régulièrement.
🎯 Côté attaquant — Face à une zone qui « autorise le HTML », l’attaquant teste tout ce qui exécute du JS sans balise
<script>:<img src=x onerror=...>,<svg onload=...>,<a href="javascript:...">, des attributson*, des balises exotiques, et des mutations (HTML malformé que le navigateur « répare » en réintroduisant du danger — mXSS). Une sanitization maison à base de regex tombe immédiatement ; seule une lib comme DOMPurify, à jour, résiste. S’il trouve une balise/attribut dangereux non filtré, c’est un stored XSS.
Encoder ou sanitizer : le bon choix
La grande majorité des affichages relèvent de l’encodage (afficher un pseudo, un titre, un message = du texte). Ne réserve la sanitization qu’aux cas où le HTML riche est un besoin réel (éditeur de contenu). Ne sanitize pas ce qui devrait être encodé (surcoût et surface inutiles), et n’encode pas ce qui doit rester du HTML (casse le formatage).
🧭 Sur FormaCampus — Sur FormaCampus, la règle est nette : texte → encodage (assuré par l’auto-échappement de React et de Twig, chapitre 7.3) ; HTML riche (le corps des cours, saisi dans un éditeur WYSIWYG par les formateurs) → DOMPurify avec une allow-list stricte (formatage, liens en
https/mailtouniquement, images internes), maintenu à jour. Le contenu riche n’est jamais rendu sans passer par la sanitization, et jamais construit en concaténant dans duinnerHTML. Une CSP (chapitre 7.4) sert de filet si une balise malveillante passait malgré tout.
✏️ Exercices
Exercice 1 — Le bon encodage. Une donnée nom doit être insérée à deux endroits : dans <h1>Bonjour NOM</h1> et dans <a href="/u?n=NOM">profil</a>. Le même encodage convient-il aux deux ? Explique.
✅ Solution
Non : l’encodage est contextuel. Pour <h1>Bonjour NOM</h1>, c’est un encodage HTML (<, >, &, guillemets) qu’il faut, pour afficher le nom comme texte. Pour <a href="/u?n=NOM">, la donnée va dans une URL : il faut un encodage URL (encodeURIComponent) du paramètre — sinon des caractères comme &, =, ", ou une séquence fermant l’attribut peuvent casser l’URL ou l’attribut. Utiliser l’encodage HTML dans l’URL (ou l’inverse) laisse des vecteurs ouverts. Chaque contexte a son encodage ; on encode au point d’insertion, pour ce contexte.
Exercice 2 — Encoder ou sanitizer ? Deux champs : (a) le pseudo d’un utilisateur, (b) le corps d’un cours rédigé dans un éditeur riche (gras, liens, listes). Pour chacun, dis s’il faut encoder ou sanitizer, et avec quoi.
✅ Solution
(a) Le pseudo est du texte : on l’encode en sortie (contexte HTML), via l’auto-échappement du framework (React/Twig) ou htmlspecialchars. Aucun HTML n’y est autorisé. (b) Le corps du cours contient du HTML riche légitime (mise en forme) : l’encoder afficherait les balises. Il faut le sanitizer avec DOMPurify (allow-list de balises/attributs sûrs, liens restreints aux schémas sûrs), lib maintenue à jour, puis le rendre en tant que HTML. Encoder détruirait le formatage ; sanitizer garde le formatage sûr et retire scripts/handlers.
🧠 Quiz de révision
1. Qu’est-ce que l’encodage de sortie et pourquoi neutralise-t-il le XSS ?
C’est transformer, au moment de l’affichage, les caractères spéciaux du contexte cible (ex. < → <) pour que la donnée soit rendue comme texte et non interprétée comme code. Comme le paramétrage SQL, il empêche la donnée de modifier la structure (le HTML) — elle reste du contenu.
2. Pourquoi dit-on que l’encodage est « contextuel » ?
Parce que la transformation correcte dépend de où la donnée est insérée : corps HTML, attribut, JavaScript, URL, CSS exigent des encodages différents. Une donnée encodée pour le HTML peut rester dangereuse dans un attribut non guillemeté ou dans une URL. On encode pour le contexte exact d’insertion.
3. Quand l’encodage ne suffit-il pas, et que fait-on alors ?
Quand on veut autoriser du HTML riche (éditeur de contenu, commentaires formatés) : encoder afficherait les balises au lieu de les appliquer. On sanitize : on garde une allow-list de balises/attributs sûrs et on supprime le reste, avec une bibliothèque éprouvée (DOMPurify), jamais avec des regex maison.
4. Pourquoi ne pas sanitizer avec des expressions régulières maison ?
Parce que le HTML est trop complexe et contournable : balises sans <script> (<img onerror>, <svg onload>), attributs on*, javascript:, HTML malformé « réparé » par le navigateur (mutation XSS). Une regex oublie toujours des cas ; seule une lib dédiée, à jour, résiste.
5. Pourquoi éviter d’insérer des données dans un contexte JavaScript ?
Parce que c’est très casse-gueule : une séquence comme </script> referme la balise et rouvre l’injection, et l’encodage JS est subtil. Mieux : passer les données du serveur au client via un data-* encodé ou un <script type="application/json"> JSON, lu proprement par le JS — ne pas « générer du JS » avec des variables concaténées.
Chapitre suivant : React, Vue & les frameworks — ce que les frameworks échappent pour toi automatiquement, et les portes précises par lesquelles le XSS revient.