L'encodage des sorties est la transformation des données utilisateur en caractères inoffensifs pour le contexte d'affichage final (HTML, JavaScript, CSS, URL, JSON), appliquée au moment précis de l'injection dans le document. C'est la défense principale contre les vulnérabilités de type Cross-Site Scripting (XSS, OWASP Top 10 A03:2021 Injection, CWE-79) et son équivalent CWE-116 (Improper Encoding or Escaping of Output). Le principe opérationnel : toute donnée provenant d'une source non sûre (utilisateur, tiers, stockage) doit être encodée selon le contexte de sortie avant d'atteindre le navigateur ou le client, jamais au moment de sa réception ou de son stockage. Cet article détaille les six contextes d'encodage distincts, la syntaxe exacte par contexte, le comportement des frameworks modernes (React, Angular, Vue, Django, Spring, Rails, Laravel), l'usage complémentaire de Content Security Policy et de DOMPurify, et les anti-patterns à éliminer.
Pourquoi encoder les sorties plutôt que filtrer les entrées
La règle OWASP Proactive Controls C4 (Encode and Escape Data) positionne sans ambiguïté l'encodage de sortie comme défense primaire contre les injections. Cinq raisons techniques justifient cette priorité.
- Une même donnée est dangereuse dans certains contextes, inoffensive dans d'autres. Un apostrophe dans un nom de famille est parfaitement légitime, mais devient une injection potentielle dans une requête SQL, un attribut HTML, ou une chaîne JavaScript. La validation à l'entrée ne peut pas anticiper tous les contextes d'utilisation future.
- Les données transitent entre systèmes avec des règles de validation hétérogènes. Un champ correctement validé par l'API web peut être importé sans validation depuis un ETL, un webhook tiers ou une synchronisation batch, court-circuitant la validation initiale.
- Les stratégies de blacklist (filtrage) sont systématiquement contournées. Les listes noires de caractères dangereux laissent passer des variantes d'encodage (UTF-7, UTF-16, null bytes, caractères Unicode homoglyphes), des normalisations (NFC, NFD), et des vecteurs découverts postérieurement.
- L'encodage est réversible sans perte. La donnée encodée stockée conserve son intégrité et son intention originales. Le filtrage supprime de l'information utile.
- L'encodage contextuel est formellement complet. Pour chacun des six contextes de sortie, le sous-ensemble de caractères à encoder est fini et documenté (OWASP XSS Prevention Cheat Sheet, DOM-based XSS Prevention Cheat Sheet). La défense est démontrable, pas probabiliste.
Les six contextes de sortie à distinguer
Chaque contexte d'injection dans le document exige son encodage propre. Confondre ou appliquer un encodage générique laisse des bypass trivialement exploitables.
| Contexte | Zone typique | Encodage requis |
|---|---|---|
| HTML body | Contenu entre balises ouvrantes et fermantes | HTML entity encoding (5 caractères critiques) |
| HTML attribute | Valeur d'attribut, strictement entre guillemets doubles | HTML attribute encoding (allow-list) |
| JavaScript data | Chaîne ou valeur injectée dans un bloc script ou événement | Unicode backslash-u escape |
| CSS | Propriété, valeur ou déclaration dans un bloc style | CSS backslash-HH hex escape |
| URL parameter | Paramètre de query string ou segment de path | Percent-encoding (encodeURIComponent) |
| JSON | Données injectées inline dans du JSON | JSON string escape natif |
Les cinq caractères critiques du contexte HTML body
| Caractère d'entrée | Entité HTML à émettre |
|---|---|
| Inférieur à | < |
| Supérieur à | > |
| Ampersand | & |
| Guillemet double | " |
| Apostrophe | ' |
Exemple d'échec à l'encodage contextuel
Un développeur qui applique uniquement un encodage HTML entity ne protège pas le contexte JavaScript. La chaîne </script><script>alert(1)</script> filtrée par htmlspecialchars passe dans un attribut onclick ou une balise script, produisant une XSS exploitable. L'encodage JavaScript approprié aurait produit </script><script>alert(1)</script>, totalement inoffensif.
Encodage par contexte : syntaxe exacte
Contexte HTML body (entre balises)
// Approche Node.js avec la librairie standard he
const he = require('he');
const userInput = '<img src=x onerror=alert(1)>';
const safeOutput = he.encode(userInput, { useNamedReferences: true });
// → <img src=x onerror=alert(1)>
// Approche alternative avec escape-html
const escapeHtml = require('escape-html');
const safe = escapeHtml(userInput);
// → <img src=x onerror=alert(1)>En Python, html.escape(s, quote=True) (standard lib) ou markupsafe.escape (Flask/Jinja) remplissent ce rôle. En Java, OWASP Encoder propose Encode.forHtml(input).
Contexte HTML attribute
Plus strict que le body : tous les caractères hors allow-list alphanumérique doivent être échappés en entité, car les attributs mal quotés sont exploitables via espaces, retours à la ligne et backticks.
// Java — OWASP Java Encoder
import org.owasp.encoder.Encode;
String safe = Encode.forHtmlAttribute(userInput);Règle d'or : toujours entourer les valeurs d'attribut de guillemets doubles, jamais d'apostrophes, jamais sans guillemets.
Contexte JavaScript data
Le contexte JavaScript est le plus complexe et le plus sensible. La seule approche sûre est de sortir la donnée en tant que valeur JSON encodée, pas directement concaténée dans du code.
// Approche sûre : sérialiser en JSON inline
// Attention : JSON.stringify seul est insuffisant si la donnée est injectée
// entre balises script, car la séquence </script> passe.
function safeJsonForScript(data) {
return JSON.stringify(data)
.replace(/</g, '\\u003c')
.replace(/>/g, '\\u003e')
.replace(/&/g, '\\u0026')
.replace(/
/g, '\\u2028')
.replace(/
/g, '\\u2029');
}
// Approche encore plus sûre : ne jamais injecter dans du script,
// transmettre via data-attribute puis lire côté JS
// HTML: <div id="app" data-config='...'></div>
// JS: const config = JSON.parse(document.getElementById('app').dataset.config);OWASP Java Encoder fournit Encode.forJavaScript(input) pour les contextes legacy. Le pattern moderne recommandé depuis 2020 évite complètement l'injection dans les blocs script au profit des attributs de données.
Contexte CSS
// OWASP CSS encoder
import { encodeForCSS } from '@owasp/encoder-js';
const safe = encodeForCSS(userInput);
// Chaque caractère spécial est encodé en \HH (hex)Règle : n'accepter l'injection CSS que pour des valeurs structurelles (largeur, couleur hex contrôlée), jamais pour des URLs ou des expressions. Utiliser une allow-list de noms de propriétés CSS acceptables.
Contexte URL
# Python — quote contre urlencode
from urllib.parse import quote, urlencode
# Pour un paramètre unique dans un path ou query
safe_param = quote(user_input, safe='')
# Pour un dict de paramètres complet
params = {'q': user_query, 'page': page_num}
safe_qs = urlencode(params)Pour les redirections basées sur un paramètre utilisateur, ajouter une vérification d'allow-list du domaine cible : l'encodage URL ne protège pas contre une redirection vers un domaine malveillant.
Contexte JSON
La sérialisation JSON native des langages modernes (JSON.stringify, json.dumps, Jackson, Newtonsoft) produit du JSON valide et sûr pour le contexte JSON pur. L'écueil principal est de concaténer manuellement : `{"user":"${username}"}` au lieu de JSON.stringify({user: username}).
Encodage dans les frameworks modernes
Les frameworks modernes auto-échappent les interpolations dans leur système de template. Comprendre leurs mécanismes et leurs échappatoires est critique pour un AppSec Engineer.
| Framework | Auto-échappement par défaut | Échappatoire explicite (à surveiller) | Risque résiduel principal |
|---|---|---|---|
| React / Next.js | Tout contenu JSX interpolé est échappé | Prop dangerouslySetInnerHTML | XSS via prop de type HTML brut |
| Angular | Interpolation template + Sanitizer auto | Bypass via DomSanitizer.bypassSecurityTrust* | XSS via bypass sanitizer explicite |
| Vue | Interpolation moustache échappée | Directive v-html | XSS via v-html sur donnée utilisateur |
| Django | Template engine échappe par défaut | Filtre safe ou appel à mark_safe | XSS via safe mal utilisé |
| Spring Thymeleaf | Attribut th:text échappe | Attribut th:utext (unescaped) | XSS via th:utext |
| Rails ERB | Erb helpers échappent depuis Rails 3 | Méthode html_safe / raw | XSS via html_safe sur user input |
| Laravel Blade | Syntaxe double-brace échappe | Syntaxe triple-brace désactive | XSS via syntaxe désactivée |
| ASP.NET Razor | Razor échappe par défaut | Helper Html.Raw | XSS via Html.Raw |
Observations clés sur les frameworks
- React protège du contexte HTML body et HTML attribute mais ne protège pas le contexte URL (attribut
href,src). Une URL de typejavascript:alert(1)passe toujours. - Angular inclut un système de
DomSanitizerplus sophistiqué qui tente de nettoyer le HTML au lieu de l'encoder. Moins sûr qu'un encoder pur, plus permissif pour les cas métier riches. - Vue a simplifié son API depuis la v3, mais
v-htmlreste l'un des top 5 vecteurs XSS en audit Vue. - Django combine auto-échappement template +
mark_safemanuel. Les vues qui construisent du HTML manuellement viaformat_htmldoivent utiliser cette fonction, pas concaténer. - Spring Thymeleaf est le plus strict des frameworks Java, auto-échappement par défaut partout. Le vecteur d'échappatoire
th:utextest facilement détectable en SAST.
Content Security Policy en complément
Content Security Policy est un en-tête HTTP qui déclare au navigateur quelles sources de scripts, styles, images et autres ressources sont autorisées. CSP est une défense complémentaire (pas substitutive) à l'encodage de sortie.
Exemple de CSP stricte moderne (CSP Level 3)
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-AB3CD9' 'strict-dynamic';
style-src 'self' 'nonce-AB3CD9';
img-src 'self' data: https:;
connect-src 'self' https://api.example.com;
object-src 'none';
base-uri 'self';
frame-ancestors 'none';
form-action 'self';
upgrade-insecure-requests;
report-uri https://csp.example.com/report;Apports de CSP
- Blocage de l'exécution de scripts inline non autorisés : une XSS stockée qui injecterait une balise script avec contenu inline est neutralisée même si l'encodage a été oublié.
- Blocage des scripts externes non whitelistés : vecteur XSS via CDN tiers compromis neutralisé.
- Blocage des eval et Function runtime (
'unsafe-eval'doit être banni). - Protection contre clickjacking via
frame-ancestors. - Signalement des violations via endpoint
report-urioureport-to.
Limites de CSP
- Ne protège pas contre les XSS qui exploitent des sources autorisées (self trust bypass).
- Ne protège pas contre les injections HTML visuelles non exécutables (phishing inline, defacement).
- Nécessite du nettoyage des scripts inline hérités : 2-4 semaines de chantier typique sur application mature.
- Les directives
'unsafe-inline'annulent l'essentiel de la protection : à bannir.
L'outil csp-evaluator.withgoogle.com fourni par Google analyse une CSP et signale les directives faibles. À intégrer dans la CI pour valider les CSP avant déploiement.
Sanitization vs encodage : quand utiliser quoi
L'encodage neutralise tous les caractères spéciaux. La sanitization laisse passer certains éléments selon une politique tout en supprimant les dangereux.
Encodage — cas d'usage
- Affichage de données utilisateur brutes : noms, emails, commentaires textuels, messages de chat.
- Cas majoritaire en 2026, systématique sauf exception documentée.
Sanitization — cas d'usage spécifiques
- Éditeurs WYSIWYG (TinyMCE, CKEditor) qui produisent du HTML riche.
- Import de Markdown transformé en HTML.
- Contenu légitime contenant du HTML partiel (newsletters, articles CMS, commentaires avec formatage).
Librairies de sanitization reconnues
| Langage / contexte | Librairie recommandée | Commentaire |
|---|---|---|
| JavaScript (client) | DOMPurify | Standard de fait, maintenu par Mario Heiderich |
| Python | Bleach | Basé sur html5lib, robuste |
| Java | OWASP Java HTML Sanitizer | Maintenu par OWASP |
| Ruby | Sanitize | Basé sur Nokogiri |
| PHP | HTML Purifier | Stricte par défaut |
| .NET | Ganss.Xss AntiXssLibrary | Moderne, alternatif à l'ancien AntiXSS |
| Go | bluemonday | Policies configurables |
Exemple d'usage DOMPurify côté React
import DOMPurify from 'dompurify';
function SafeHtmlContent({ markdownHtml }) {
// markdownHtml est le résultat d'un parser Markdown sur user input
const clean = DOMPurify.sanitize(markdownHtml, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br', 'code', 'pre'],
ALLOWED_ATTR: ['href', 'title'],
ALLOWED_URI_REGEXP: /^(?:https?|mailto):/i,
});
// dangerouslySetInnerHTML acceptable UNIQUEMENT après sanitize par librairie reconnue
return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}Pièges et anti-patterns à éliminer
Utiliser innerHTML avec des données utilisateur. Vecteur XSS DOM le plus fréquent. Utiliser textContent pour du texte pur, ou DOMPurify + innerHTML si du HTML est nécessaire.
Appeler document.write() ou eval() avec du contenu dynamique. Interdits en secure coding moderne. eval doit être banni via règle ESLint ou CSP 'unsafe-eval' retiré. document.write est obsolète.
Concaténer des URL avec des entrées utilisateur sans encodage. `https://api.example.com/search?q=${q}` injecte une XSS si q contient ¶m=, une redirection ouverte, ou une bombe de requête. Utiliser URL ou URLSearchParams natifs.
Double-encoder ou décoder accidentellement. Une donnée encodée HTML puis re-encodée produit &amp; au lieu de &. Identifier clairement la frontière d'encodage et ne pas la traverser deux fois.
Traiter les données de stockage comme sûres. Une donnée validée à la saisie peut être modifiée plus tard (migration, import, manipulation directe en base). Toujours encoder à la sortie, même pour des données jugées sûres.
Encoder côté base de données. Stocker des données déjà encodées HTML rend le stockage ambigu et empêche les usages non-HTML futurs (export CSV, email texte brut, API JSON). Stocker en clair, encoder uniquement à la sortie.
Se fier au typage du framework pour échapper automatiquement les attributs href. React, Angular et Vue encodent le texte mais n'empêchent pas href="javascript:...". Ajouter une vérification explicite du protocole (allow-list http, https, mailto, tel).
Utiliser un encodage unique pour tous les contextes. htmlspecialchars en PHP protège HTML body et attribute mais laisse des vecteurs en JavaScript, CSS et URL. Un encoder contextuel est obligatoire.
Outils et librairies recommandés
| Catégorie | Outil / librairie | Langage / plateforme |
|---|---|---|
| Encoder serveur | OWASP Java Encoder | Java |
| Encoder serveur | he, escape-html | Node.js |
| Encoder serveur | html (stdlib), markupsafe | Python |
| Encoder serveur | ERB::Util.html_escape | Ruby |
| Sanitizer client | DOMPurify | JavaScript navigateur |
| Sanitizer serveur | OWASP Java HTML Sanitizer, Bleach, HTML Purifier | Java, Python, PHP |
| SAST règles XSS | Semgrep p/xss, CodeQL js/xss | Multi-langages |
| Linter | eslint-plugin-react (no-danger), eslint-plugin-no-unsanitized | JavaScript / React |
| Test DAST | OWASP ZAP, Burp Suite Pro Scanner | Pentest |
| Analyse CSP | csp-evaluator.withgoogle.com, Mozilla Observatory | Audit |
| Monitoring CSP | report-uri.com, CSP endpoint custom | Production |
Points clés à retenir
- Encodage de sortie = défense primaire contre XSS (OWASP Top 10 A03, CWE-79, CWE-116), complémentaire et non alternative à la validation d'entrée.
- Six contextes distincts : HTML body, HTML attribute, JavaScript data, CSS, URL, JSON — chacun exige son encodage propre.
- Frameworks modernes auto-échappent 80-90 % des cas, mais laissent des échappatoires explicites (
dangerouslySetInnerHTML,v-html,th:utext,mark_safe,html_safe) à surveiller en SAST et code review. - CSP en défense complémentaire de dernière ligne : nonce + strict-dynamic, bannir
unsafe-inlineetunsafe-eval. - Sanitization uniquement pour les cas légitimes de HTML riche (WYSIWYG, Markdown) via librairie reconnue (DOMPurify, OWASP Java HTML Sanitizer, Bleach), jamais en regex maison.
- Stocker en clair, encoder à la sortie : règle d'or pour préserver la flexibilité multi-contexte.
- Vérifier les URL et protocoles explicitement : frameworks encodent le texte mais pas les schemes
javascript:oudata:. - SAST + Code review + CSP + DOMPurify constituent la défense en profondeur standard en 2026.
Pour aller plus loin
- Secure coding : définition, principes et référentiels 2026 — principes fondateurs incluant l'encodage de sortie dans un cadre plus large.
- Vulnérabilité IDOR : définition, exemples et prévention 2026 — complément sur la famille OWASP Top 10 A01 Broken Access Control.
- Introduction à l'OWASP Top 10 — panorama de tous les risques majeurs dont A03 Injection (incluant XSS).
- Roadmap secure coding 2026 — parcours d'apprentissage pour maîtriser les contre-mesures dont l'encodage contextuel.
- Roadmap AppSec 2026 — trajectoire complète AppSec Engineer confronté quotidiennement à ces choix d'encodage.







