Chaque discussion de sécurité web finit par la même diapositive : échappe toujours l’entrée utilisateur avant de l’insérer dans HTML. La raison sont les entités HTML — le mécanisme qui transforme <script> en <script> et empêche le commentaire d’un utilisateur de devenir une attaque XSS.
Mais l’échappement n’est que la moitié de l’histoire. Le déséchappement — transformer & en & — est tout aussi important et tout aussi dangereux quand c’est mal fait. Cet article couvre les deux directions, les caractères qui comptent, et les erreurs qui mènent à de vraies vulnérabilités.
Les cinq caractères qui comptent
HTML a cinq caractères qui sont structurellement significatifs — ils délimitent les balises, les attributs et les entités. Si l’un d’eux apparaît dans le contenu utilisateur et n’est pas échappé, le navigateur les interprète comme du balisage HTML :
| Caractère | Entité | Pourquoi c’est important |
|---|---|---|
< |
< |
Démarre une balise. <script> est une balise. |
> |
> |
Termine une balise. |
& |
& |
Démarre une entité. < est une entité. |
" |
" |
Délimite les valeurs d’attribut. |
' |
' |
Délimite les attributs à guillemets simples. |
Si vous insérez du texte dans le contenu d’un élément HTML, vous devez échapper <, > et &. Si vous insérez du texte dans une valeur d’attribut, vous devez aussi échapper " (ou ' si l’attribut utilise des guillemets simples).
Comment fonctionnent les entités
Une entité HTML est une séquence spéciale que le navigateur interprète comme un seul caractère :
- Entités nommées :
&,<,>,", — faciles à lire, ensemble limité. - Entités numériques :
<(décimal),<(hex) — n’importe quel caractère Unicode par point de code. - Entités nommées pour les symboles :
©(©),€(€),—(—) — noms de commodité.
L’entité & est spéciale car c’est l’échappement pour & lui-même. Si vous échappez & d’abord, puis échappez < et >, vous n’avez pas de double échappement. L’ordre compte :
Correct : & → & → &lt; (correct)
Incorrect : < → < → &lt; (double échappement)
Échappez toujours & en premier. Puis les autres caractères. Ainsi vous n’aurez pas de double échappement.
Où l’échappement est requis
Contenu généré par l’utilisateur. Commentaires, messages de forum, avis, messages — tout texte provenant d’un utilisateur et apparaissant sur une page doit être échappé. Un commentaire contenant <img src=x onerror=alert(1)> est une attaque XSS si vous n’échappez pas < et >.
Données dans les attributs. Si vous mettez des données utilisateur dans un attribut href, src, alt ou title, échappez les caractères d’entité. Une valeur comme foo" onclick="alert(1)" sort de l’attribut et injecte un gestionnaire d’événements.
Interpolation de chaînes JavaScript. Si vous construisez HTML en JavaScript :
// DANGEREUX : interpolation brute
element.innerHTML = `<p>${userComment}</p>`;
// SÛR : interpolation échappée
element.innerHTML = `<p>${escapeHtml(userComment)}</p>`;
innerHTML interprète la chaîne comme HTML. Si userComment contient du balisage, il s’exécute. Échappez toujours avant d’interpoler dans HTML.
Rendu côté serveur. Les moteurs de templates (Handlebars, Jinja, EJS, Razor) échappent généralement automatiquement par défaut. Mais si vous utilisez un filtre “safe” ou “raw” (comme le triple crochet de Handlebars {{{ ou |safe de Django), vous désactivez l’échappement. Ne le faites que lorsque vous contrôlez le contenu.
Quand déséchapper
Le déséchappement inverse le processus : < → <, & → &. C’est nécessaire quand :
- Affichage de données encodées. Si une API retourne du HTML échappé (certaines le font pour empêcher le rendu), vous devez déséchapper avant d’afficher.
- Traitement des données de formulaire. Les soumissions de formulaire peuvent contenir des valeurs encodées en URL ou encodées en entités.
- Analyse de l’entrée utilisateur. Si un utilisateur colle du texte encodé en HTML et que vous voulez l’afficher tel quel, déséchappez d’abord — puis ré-échappez si vous l’insérez à nouveau dans HTML.
Le danger : déséchappez seulement quand vous allez afficher, jamais quand vous allez stocker. Stockez la forme originale échappée. Déséchappez au moment du rendu.
XSS : à un caractère échappé de distance
Le cross-site scripting (XSS) se produit lorsque l’entrée utilisateur non échappée atteint l’analyseur HTML du navigateur. Les variantes les plus courantes :
XSS stocké. Un utilisateur soumet <script>steal(document.cookie)</script> comme commentaire. Le serveur le stocke non échappé. Chaque visiteur qui voit le commentaire exécute le script.
XSS réfléchi. Une URL contient ?q=<script>alert(1)</script>. Le serveur réfléchit le paramètre q dans la page sans l’échapper. N’importe qui clique sur le lien exécute le script.
XSS basé sur le DOM. JavaScript lit location.hash et l’insère dans le DOM via innerHTML. Le hash contient du balisage. Le navigateur l’affiche.
Les trois sont prévenus par la même chose : échappez <, >, &, " et ' avant d’insérer du contenu dans HTML.
La fonction d’échappement
Chaque langage en a une. Le pattern est le même :
function escapeHtml(str) {
return str
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
L’ordre compte : & en premier, puis les autres. Si vous échappez < avant &, vous obtenez &lt; au lieu de <.
Pour les systèmes à haut débit, utilisez une bibliothèque. DOMPurify assainit HTML (en autorisant les balises sûres, en supprimant les dangereuses). Le DOMParser intégré de la plateforme web et textContent gèrent aussi l’échappement implicitement.
Erreurs courantes
- Échapper
&après<— cause un double échappement :&lt;au lieu de<. - Oublier le contexte d’attribut — échapper
<et>mais pas"permet l’injection d’attributs. - Utiliser
innerHTMLsans échappement — le vecteur XSS le plus courant. - Faire confiance aux réponses serveur — les APIs qui retournent du HTML “assaini” peuvent ne pas tout assainir. Échappez côté client aussi.
- Déséchapper trop tôt — déséchapper au moment du stockage signifie que le payload brut reste dans votre base de données, prêt à s’exécuter si le rendu oublie de ré-échapper.
Essayez
Si vous avez du texte encodé en HTML à décoder ou du texte brut à échapper pour une insertion sûre dans HTML, utilisez un outil basé sur navigateur pour que la conversion reste locale.