Skip to Content

Chapitre 7.3 — React, Vue & les frameworks

⏱️ TL;DR — Bonne nouvelle : les frameworks modernes auto-échappent le texte que tu affiches. En React/JSX, {maVariable} est encodé automatiquement pour le contexte HTML — un <script> dans une donnée s’affiche comme texte. Idem pour Vue ({{ }}), Angular, Svelte, et Twig côté Symfony ({{ }} échappe par défaut). Le XSS repasse par les portes explicites que ces frameworks fournissent pour injecter du HTML brut : dangerouslySetInnerHTML (React), v-html (Vue), le filtre |raw (Twig), [innerHTML] (Angular). Ces portes court-circuitent l’échappement : dès que tu les utilises sur une donnée non fiable, tu recrées un XSS — sauf si tu sanitizes d’abord (DOMPurify). Autre vecteur : les href/src en javascript:.

🎯 Objectifs

  • Savoir ce que React, Vue, Twig échappent automatiquement (et le contexte).
  • Identifier les portes d’injection HTML brut qui rouvrent le XSS et quand elles sont sûres.
  • Sécuriser les cas légitimes de HTML riche (sanitizer avant dangerouslySetInnerHTML).
  • Reconnaître les vecteurs résiduels : href/src en javascript:, injection dans les attributs, SSR.

Ce que les frameworks font pour toi

La raison pour laquelle le XSS est moins fréquent dans les apps React/Vue/Symfony modernes qu’à l’époque du PHP « echo brut », c’est l’auto-échappement par défaut. Quand tu interpoles une variable dans le rendu, le framework l’encode pour le HTML avant de l’insérer :

// React/JSX : {nom} est auto-echappe pour le contexte HTML function Profil({ nom }) { return <h1>Bonjour {nom}</h1> } // si nom = "<script>alert(1)</script>", React affiche le TEXTE, n'execute rien
{# Twig (Symfony) : {{ }} echappe par defaut #} <h1>Bonjour {{ nom }}</h1> {# <script> devient du texte affiche #}
<!-- Vue : {{ }} (moustache) auto-echappe --> <h1>Bonjour {{ nom }}</h1>

C’est le bon défaut (secure by default) : le cas courant — afficher du texte — est sûr sans que tu aies à y penser. Tant que tu restes dans l’interpolation normale, tu es protégé du XSS HTML de base. Mais ce filet a des trous connus, par lesquels le XSS revient.

Les portes qui rouvrent le XSS

Chaque framework fournit une porte explicite pour injecter du HTML brut (non échappé) — nécessaire pour afficher du contenu formaté. Ces portes sont volontairement nommées pour alerter, et les utiliser sur une donnée non fiable recrée un XSS.

FrameworkPorte d’injection HTML brutNom qui doit alerter
ReactdangerouslySetInnerHTML={{ __html: x }}« dangerously » — le mot est un avertissement
Vuev-html="x"contourne l’échappement moustache
Angular[innerHTML]="x" (Angular a un sanitizer intégré, mais bypassSecurityTrust* le désactive)bypassSecurityTrust* = danger explicite
Svelte{@html x}injection brute
Twig{{ x|raw }}le filtre raw désactive l’échappement
// ❌ XSS : donnee non fiable injectee en HTML brut <div dangerouslySetInnerHTML={{ __html: commentaireUtilisateur }} /> // commentaireUtilisateur = "<img src=x onerror=vol()>" -> EXECUTE // ✅ Si tu DOIS afficher du HTML riche : SANITIZE d'abord import DOMPurify from 'dompurify' <div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(commentaireUtilisateur) }} />

La règle : n’utilise ces portes que pour du HTML riche légitime, et toujours après sanitization (DOMPurify — chapitre 7.2). Ne les utilise jamais pour afficher du texte simple (l’interpolation normale suffit et est sûre). Chaque usage de dangerouslySetInnerHTML/v-html/|raw devrait être rare, justifié, et accompagné d’une sanitization visible.

⚠️ Piège — Le piège le plus courant en React : utiliser dangerouslySetInnerHTML « juste pour afficher un peu de texte avec un retour à la ligne » ou « parce que la donnée contient déjà du HTML ». Si la donnée vient (même indirectement) d’un utilisateur et n’est pas sanitizée, c’est un XSS. Demande-toi toujours : « ai-je vraiment besoin de HTML ici, ou juste de texte ? ». Si texte : interpolation normale. Si HTML riche : DOMPurify d’abord, sans exception.

Le vecteur href / src en javascript:

Même sans injecter de HTML brut, un XSS peut passer par une URL contrôlée placée dans un href ou src. Le schéma javascript: exécute du code au clic :

// ❌ Si `lien` vaut "javascript:vol()", cliquer le lien execute le code <a href={lienUtilisateur}>Mon site</a>

React ne neutralise pas automatiquement ce cas (il échappe le texte, pas la sémantique de l’URL — même si des avertissements existent pour javascript:). Parade : valider le schéma de l’URL avant de l’utiliser — n’autoriser que https:, http:, mailto: (allow-list), rejeter javascript:, data:, vbscript:.

// ✅ Valider le schema avant d'utiliser une URL fournie function urlSure(u) { try { const parsed = new URL(u, window.location.origin) return ['https:', 'http:', 'mailto:'].includes(parsed.protocol) ? u : '#' } catch { return '#' } } <a href={urlSure(lienUtilisateur)}>Mon site</a>

Cas particuliers à connaître

  • SSR / hydratation (Next.js) : le rendu côté serveur applique le même auto-échappement pour l’interpolation, mais les mêmes portes (dangerouslySetInnerHTML) restent dangereuses côté serveur et client. On verra la sécurité Next.js en détail en Partie 10.
  • Attributs dynamiques : injecter une donnée dans certains attributs sensibles (style, srcdoc, gestionnaires on*) peut créer des vecteurs même via le framework. Reste sur des attributs « données » et évite de piloter des attributs exécutables avec de l’entrée.
  • Rendu de Markdown : un rendu Markdown → HTML doit être sanitizé (beaucoup de moteurs autorisent le HTML inline), sinon c’est un vecteur classique.
  • Traductions / i18n : des chaînes de traduction contenant du HTML, interpolées avec des variables utilisateur, sont un piège récurrent.

💡 Réflexe — Fais de chaque dangerouslySetInnerHTML / v-html / |raw un point d’attention en revue de code : cherche-les (grep), et pour chacun, vérifie que la donnée est sanitizée et que le HTML est vraiment nécessaire. Ces mots-clés sont les sinks du XSS dans une app à framework : les auditer, c’est couvrir l’essentiel du risque. Ajoute une règle de linter (ex. react/no-danger) qui les signale automatiquement.

🧭 Sur FormaCampus — Le front de FormaCampus (Next.js/React) s’appuie sur l’auto-échappement de JSX pour tout le texte (pseudos, titres, messages) — l’essentiel de l’affichage est ainsi sûr sans effort. Les seuls dangerouslySetInnerHTML de la codebase concernent le corps des cours (HTML riche), et chacun passe par DOMPurify juste avant ; une règle ESLint (react/no-danger) signale tout nouvel usage en revue. Les liens fournis par les utilisateurs (site perso d’un formateur) sont validés par allow-list de schéma (https/mailto). Côté back, les templates Twig échappent par défaut, et |raw est proscrit sur toute donnée utilisateur.

✏️ Exercices

Exercice 1 — Où est le XSS ? Dans ce composant React, identifie la ligne à risque et corrige.

function Commentaire({ auteur, texteHtml }) { return ( <div> <strong>{auteur}</strong> <div dangerouslySetInnerHTML={{ __html: texteHtml }} /> </div> ) }

✅ Solution

La ligne <strong>{auteur}</strong> est sûre (auto-échappement JSX). Le risque est le dangerouslySetInnerHTML={{ __html: texteHtml }} : si texteHtml vient d’un utilisateur et n’est pas sanitizé, un <img src=x onerror=...> s’exécute → stored XSS. Deux corrections possibles : (1) si le texte n’a pas besoin d’être du HTML, remplacer par <div>{texte}</div> (interpolation échappée) ; (2) si le HTML riche est nécessaire (commentaire formaté), sanitizer : dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(texteHtml) }}. Ne jamais injecter texteHtml brut. Ajouter une règle ESLint react/no-danger pour repérer ces usages.

Exercice 2 — Le lien piégé. Un profil permet à l’utilisateur de renseigner l’URL de son site, affichée ainsi : <a href={site}>mon site</a>. Pourquoi est-ce risqué et comment corriger ?

✅ Solution

Risque : React échappe le texte mais ne contrôle pas la sémantique de l’URL ; si site vaut javascript:vol(), cliquer le lien exécute le code (XSS). Correctif : valider le schéma avant d’utiliser l’URL — n’autoriser qu’une allow-list (https:, http:, mailto:) et remplacer par #/rien sinon. Par exemple, parser avec new URL(...) et vérifier protocol. On rejette explicitement javascript:, data:, vbscript:. Ne jamais placer une URL fournie par l’utilisateur dans un href/src sans valider son schéma.

🧠 Quiz de révision

1. Que font React, Vue et Twig par défaut vis-à-vis du XSS ?

Ils auto-échappent le texte interpolé pour le contexte HTML : {x} (React/JSX), {{ x }} (Vue, Twig) encodent la donnée, donc un <script> s’affiche comme texte au lieu d’être exécuté. C’est un bon défaut (secure by default) pour le cas courant : afficher du texte.

2. Par quelles portes le XSS revient-il dans une app à framework ?

Par les injections de HTML brut : dangerouslySetInnerHTML (React), v-html (Vue), [innerHTML]/bypassSecurityTrust* (Angular), {@html} (Svelte), le filtre |raw (Twig). Elles court-circuitent l’auto-échappement ; sur une donnée non fiable non sanitizée, elles recréent un XSS.

3. Comment utiliser dangerouslySetInnerHTML en sécurité ?

Seulement pour du HTML riche légitime, et toujours après sanitization (DOMPurify.sanitize(...)). Jamais pour du texte simple (l’interpolation normale suffit et est sûre). Chaque usage doit être rare, justifié et accompagné d’une sanitization visible.

4. Pourquoi un href fourni par l’utilisateur est-il un vecteur ?

Parce que le schéma javascript: dans un href/src exécute du code au clic, et le framework échappe le texte mais pas la sémantique de l’URL. Parade : valider le schéma par allow-list (https/http/mailto), rejeter javascript:/data:.

5. Quel réflexe de revue de code couvre l’essentiel du risque XSS en app à framework ?

Rechercher et auditer chaque dangerouslySetInnerHTML/v-html/|raw (ce sont les sinks) : vérifier que la donnée est sanitizée et que le HTML est vraiment nécessaire. Idéalement, une règle de linter (react/no-danger) les signale automatiquement.


Chapitre suivant : CSP & Trusted Types — le filet de sécurité qui rattrape les XSS passés à travers l’encodage et la sanitization.

Last updated on