OWASP & AppSec

Encodage des sorties : bonnes pratiques XSS et injection 2026

Encodage des sorties en secure coding : contextes HTML, JS, CSS, URL. Prévention XSS, anti-patterns, frameworks React, Django, Spring, CSP et DOMPurify.

Naim Aouaichia
14 min de lecture
  • Encodage sorties
  • Output encoding
  • XSS
  • Secure coding
  • OWASP
  • CSP
  • DOMPurify
  • React
  • Django
  • Spring
  • AppSec
  • Prévention
  • Best practices
  • Contextual encoding

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é.

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.

ContexteZone typiqueEncodage requis
HTML bodyContenu entre balises ouvrantes et fermantesHTML entity encoding (5 caractères critiques)
HTML attributeValeur d'attribut, strictement entre guillemets doublesHTML attribute encoding (allow-list)
JavaScript dataChaîne ou valeur injectée dans un bloc script ou événementUnicode backslash-u escape
CSSPropriété, valeur ou déclaration dans un bloc styleCSS backslash-HH hex escape
URL parameterParamètre de query string ou segment de pathPercent-encoding (encodeURIComponent)
JSONDonnées injectées inline dans du JSONJSON string escape natif

Les cinq caractères critiques du contexte HTML body

Caractère d'entréeEntité 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 });
// → &lt;img src=x onerror=alert(1)&gt;
 
// Approche alternative avec escape-html
const escapeHtml = require('escape-html');
const safe = escapeHtml(userInput);
// → &lt;img src=x onerror=alert(1)&gt;

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.

FrameworkAuto-échappement par défautÉchappatoire explicite (à surveiller)Risque résiduel principal
React / Next.jsTout contenu JSX interpolé est échappéProp dangerouslySetInnerHTMLXSS via prop de type HTML brut
AngularInterpolation template + Sanitizer autoBypass via DomSanitizer.bypassSecurityTrust*XSS via bypass sanitizer explicite
VueInterpolation moustache échappéeDirective v-htmlXSS via v-html sur donnée utilisateur
DjangoTemplate engine échappe par défautFiltre safe ou appel à mark_safeXSS via safe mal utilisé
Spring ThymeleafAttribut th:text échappeAttribut th:utext (unescaped)XSS via th:utext
Rails ERBErb helpers échappent depuis Rails 3Méthode html_safe / rawXSS via html_safe sur user input
Laravel BladeSyntaxe double-brace échappeSyntaxe triple-brace désactiveXSS via syntaxe désactivée
ASP.NET RazorRazor échappe par défautHelper Html.RawXSS 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 type javascript:alert(1) passe toujours.
  • Angular inclut un système de DomSanitizer plus 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-html reste l'un des top 5 vecteurs XSS en audit Vue.
  • Django combine auto-échappement template + mark_safe manuel. Les vues qui construisent du HTML manuellement via format_html doivent 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:utext est 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-uri ou report-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 / contexteLibrairie recommandéeCommentaire
JavaScript (client)DOMPurifyStandard de fait, maintenu par Mario Heiderich
PythonBleachBasé sur html5lib, robuste
JavaOWASP Java HTML SanitizerMaintenu par OWASP
RubySanitizeBasé sur Nokogiri
PHPHTML PurifierStricte par défaut
.NETGanss.Xss AntiXssLibraryModerne, alternatif à l'ancien AntiXSS
GobluemondayPolicies 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 &param=, 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;amp; au lieu de &amp;. 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égorieOutil / librairieLangage / plateforme
Encoder serveurOWASP Java EncoderJava
Encoder serveurhe, escape-htmlNode.js
Encoder serveurhtml (stdlib), markupsafePython
Encoder serveurERB::Util.html_escapeRuby
Sanitizer clientDOMPurifyJavaScript navigateur
Sanitizer serveurOWASP Java HTML Sanitizer, Bleach, HTML PurifierJava, Python, PHP
SAST règles XSSSemgrep p/xss, CodeQL js/xssMulti-langages
Lintereslint-plugin-react (no-danger), eslint-plugin-no-unsanitizedJavaScript / React
Test DASTOWASP ZAP, Burp Suite Pro ScannerPentest
Analyse CSPcsp-evaluator.withgoogle.com, Mozilla ObservatoryAudit
Monitoring CSPreport-uri.com, CSP endpoint customProduction

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-inline et unsafe-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: ou data:.
  • SAST + Code review + CSP + DOMPurify constituent la défense en profondeur standard en 2026.

Pour aller plus loin

Questions fréquentes

  • Pourquoi encoder les sorties plutôt que valider les entrées ?
    Les deux sont nécessaires, mais l'encodage des sorties est la défense ultime contre les injections (XSS principalement). La validation des entrées réduit la surface d'attaque mais ne peut pas tout couvrir : une donnée légitime pour un contexte (un apostrophe dans un nom de famille) peut devenir dangereuse dans un autre (injectée dans une requête SQL ou un attribut JavaScript). L'encodage contextuel des sorties transforme les caractères spéciaux en équivalents sûrs pour le contexte d'affichage final, garantissant que la donnée ne sera jamais interprétée comme du code. La règle OWASP Proactive Controls C4 (Encode and Escape Data) est claire : toute donnée provenant d'une source non sûre doit être encodée selon le contexte de sortie au moment de son injection dans le document, pas au moment de sa réception. Les deux contrôles combinés (validation d'entrée côté serveur + encodage contextuel de sortie) constituent la défense en profondeur minimale contre XSS et injections similaires.
  • Quels sont les contextes d'encodage à distinguer en 2026 ?
    Six contextes principaux exigent chacun un encodage spécifique. 1) HTML body (entre balises) : encodage HTML entity pour les cinq caractères critiques (inférieur, supérieur, ampersand, guillemets simples et doubles). 2) HTML attribute : encodage attribute avec échappement strict, toujours entre guillemets doubles. 3) JavaScript data : encodage Unicode backslash-u pour les chaînes injectées dans du JS, interdiction d'injection dans une fonction eval ou setTimeout. 4) CSS : encodage hexadécimal backslash-HH pour les valeurs de propriété, interdiction d'injection dans une expression ou un import URL. 5) URL : percent-encoding via encodeURIComponent pour les paramètres, allow-list stricte pour les hosts de redirection. 6) JSON : échappement JSON natif (via JSON.stringify côté client, json.dumps côté Python, Jackson côté Java). L'erreur fréquente est d'utiliser un encodage générique (typiquement htmlspecialchars PHP) pour tous les contextes, laissant des vecteurs d'injection exploitables dans les attributs JavaScript, CSS et URL.
  • Les frameworks modernes comme React suffisent-ils à protéger des XSS ?
    React, Angular, Vue et les frameworks modernes côté serveur (Django, Spring Thymeleaf, Rails, Laravel Blade) échappent par défaut les interpolations dans le template. Ils couvrent 80-90 % des cas de XSS classiques, mais laissent cinq vecteurs résiduels courants en 2026. 1) Les échappatoires intentionnelles (dangerouslySetInnerHTML en React, innerHTML en Angular, v-html en Vue, filtre safe en Django, th:utext en Thymeleaf, html_safe en Rails). 2) Les attributs URL (href, src) où l'encodage HTML ne protège pas contre les URL javascript:. 3) Les contextes style et événements inline (onclick, onload). 4) Les XSS DOM côté client via manipulation directe du DOM en JavaScript. 5) Les frameworks côté serveur avec Ajax injectant du HTML sans repasser par le système de templating. Un audit AppSec annuel identifie systématiquement des bypass de ces cinq catégories même sur des codebases rigoureuses.
  • Quelle différence entre sanitization et encodage en 2026 ?
    L'encodage transforme tous les caractères spéciaux en équivalents inertes pour le contexte de sortie : la donnée est intégralement préservée mais ne peut pas être interprétée comme du code. La sanitization analyse la donnée, identifie les éléments dangereux selon une politique, et supprime ou neutralise uniquement ces éléments : la donnée est partiellement altérée, le reste est conservé comme actif dans le contexte. Cas d'usage typique : un commentaire de blog qui doit supporter du Markdown ou un sous-ensemble HTML (balises texte en gras, italique, liens) exige la sanitization (DOMPurify côté client, OWASP Java HTML Sanitizer côté serveur, Bleach côté Python) car l'encodage brut détruirait le formatage. En dehors de ce cas, privilégier systématiquement l'encodage, moins risqué et plus performant. Jamais de blacklist maison pour sanitize : toujours une librairie reconnue et mise à jour qui suit les nouveaux vecteurs d'attaque.
  • Content Security Policy est-elle une alternative à l'encodage ?
    Non, une défense complémentaire. CSP (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 à être chargées et exécutées. Une CSP stricte (directive script-src avec nonce ou strict-dynamic) bloque l'exécution de scripts injectés y compris quand l'encodage de sortie a été oublié, constituant une défense de dernière ligne. Mais CSP ne protège pas contre : les XSS qui exploitent des sources autorisées (trusted script source hijacking), les injections HTML qui n'exécutent pas de JavaScript (phishing visuel, clickjacking via iframe), les fuites de données via attributs passifs. La CSP Level 3 recommandée en 2026 combine script-src avec nonce ou hash dynamique, object-src none, base-uri self, frame-ancestors none. La mise en place d'une CSP strict-dynamic typique prend 2-4 semaines sur une application mature en raison du nettoyage nécessaire des scripts inline et des eval héritées.
  • Quels outils utiliser pour vérifier l'encodage des sorties d'une application ?
    Trois couches complémentaires en 2026. 1) En développement : linters qui détectent les échappatoires dangereuses. ESLint avec plugins react/no-danger et jsx-a11y, Sonar avec règles security hotspot, Semgrep avec règles p/react-security et p/xss. 2) En CI automatisé : SAST (Semgrep, GitHub CodeQL, Snyk Code, SonarQube) qui analysent les chemins de flux de données (taint analysis) depuis les sources utilisateur jusqu'aux points de sortie. 3) En pentest : Burp Suite Pro Scanner + extension XSSMap / XSStrike, OWASP ZAP Active Scanner, DAST commercial comme Invicti. Pour la validation spécifique de CSP : csp-evaluator.withgoogle.com (outil Google), Mozilla Observatory, securityheaders.com. Un pipeline mature combine linter pre-commit + SAST CI + DAST staging + pentest annuel + programme bug bounty.

Écrit par

Naim Aouaichia

Expert cybersécurité et fondateur de Zeroday Cyber Academy

Expert cybersécurité avec un master spécialisé et un parcours hybride : développement, DevOps, DevSecOps, SOC, GRC. Fondateur de Hash24Security et Zeroday Cyber Academy. Formateur et créateur de contenu technique sur la cybersécurité appliquée, la sécurité des LLM et le DevSecOps.