À propos de ce cours
Apprenez à protéger vos pages contre les failles XSS (Cross-Site Scripting) : notion de donnée non fiable, XSS stockée, réfléchie et basée sur le DOM, dangers de innerHTML, affichage sûr avec textContent et createElement, vérification des URL, nettoyage avec DOMPurify, pièges de eval et des secrets dans le code, intégrité des scripts (SRI) et Content Security Policy. Avec une liste de contrôle et des activités corrigées.
-
Objectifs
- Comprendre ce qu'est une faille XSS (Cross-Site Scripting) et ses conséquences.
- Savoir pourquoi
innerHTMLest dangereux avec des données non fiables. - Afficher des données en toute sécurité avec
textContent,createElementet des URL vérifiées. - Connaître les protections complémentaires : nettoyage (DOMPurify), CSP, SRI, et la règle « ne jamais faire confiance au client ».
-
Qu'est-ce qu'une donnée non fiable ?
- Est non fiable toute donnée qui ne vient pas de votre propre code : saisie d'un formulaire, paramètre de l'adresse (
?recherche=...), partie après le#, réponse d'une API, contenu dulocalStorage, nom de fichier envoyé, message d'un autre utilisateur… - Règle d'or : toute donnée extérieure peut contenir du code malveillant. On ne l'insère jamais dans la page comme du HTML sans précaution.
- Est non fiable toute donnée qui ne vient pas de votre propre code : saisie d'un formulaire, paramètre de l'adresse (
-
La faille XSS
- Une faille XSS permet à un attaquant de faire exécuter son JavaScript dans la page d'un autre utilisateur. Ce script peut alors :
- lire les données de la page et du
localStorage(jetons de connexion, informations personnelles) ; - agir à la place de l'utilisateur (envoyer un message, modifier un mot de passe) ;
- afficher un faux formulaire de connexion pour voler des identifiants ;
- rediriger vers un site piégé.
- lire les données de la page et du
- Les trois formes de XSS :
Type Principe Exemple Stockée Le code malveillant est enregistré sur le serveur puis affiché à tous les visiteurs. Un commentaire de livre d'or contenant une balise piégée. Réfléchie Le code est dans un lien envoyé à la victime et renvoyé par le serveur dans la page. site.com/recherche?q=<img src=x onerror=...>Basée sur le DOM C'est le JavaScript de la page lui-même qui insère la donnée dangereuse. element.innerHTML = location.hash
- Une faille XSS permet à un attaquant de faire exécuter son JavaScript dans la page d'un autre utilisateur. Ce script peut alors :
-
Pourquoi innerHTML est dangereux
innerHTMLdemande au navigateur d'interpréter la chaîne comme du HTML. Si la chaîne vient d'un utilisateur, il peut y glisser des balises et des attributs d'événement :
// Message saisi par un « visiteur » const commentaire = '<img src="x" onerror="alert(\'Votre session est volée\')">'; // ✘ DANGEREUX : l'image n'existe pas, onerror s'exécute document.querySelector("#commentaires").innerHTML += `<p>${commentaire}</p>`;- À noter : une balise
<script>insérée avecinnerHTMLne s'exécute pas. Cela ne protège de rien : les attributs commeonerror,onloadouonmouseover, eux, s'exécutent. - Les mêmes risques existent avec
outerHTML,insertAdjacentHTML(),document.write()et les fonctions de jQuery comme.html()ou$("<div>" + saisie + "</div>").
-
Afficher des données en toute sécurité
1. textContent pour du texte
// ✔ SÛR : la chaîne est affichée telle quelle, rien n'est interprété
const paragraphe = document.createElement("p");
paragraphe.textContent = commentaire;
document.querySelector("#commentaires").append(paragraphe);
2. Construire les éléments au lieu d'assembler du HTML
function creerCarteCommentaire(auteur, message) {
const carte = document.createElement("div");
carte.className = "card mb-2";
const corps = document.createElement("div");
corps.className = "card-body";
const titre = document.createElement("h5");
titre.className = "card-title";
titre.textContent = auteur; // donnée utilisateur → textContent
const texte = document.createElement("p");
texte.className = "card-text";
texte.textContent = message; // donnée utilisateur → textContent
corps.append(titre, texte);
carte.append(corps);
return carte;
}
3. Vérifier les adresses (liens et images)
- Un lien dont l'adresse vient d'un utilisateur peut contenir
javascript:...: le code s'exécute au clic. On n'accepte quehttp:ethttps::
function creerLienSur(adresse, texte) {
const lien = document.createElement("a");
lien.textContent = texte;
try {
const url = new URL(adresse, location.href);
if (url.protocol === "http:" || url.protocol === "https:") {
lien.href = url.href;
}
} catch {
// adresse invalide : on ne met pas de lien
}
return lien;
}
4. Quand on a vraiment besoin de HTML : le nettoyer
- Pour afficher du HTML venant d'un éditeur de texte riche, on utilise une bibliothèque de nettoyage reconnue comme DOMPurify, qui retire scripts et attributs dangereux :
// après avoir chargé DOMPurify (npm install dompurify ou CDN)
zone.innerHTML = DOMPurify.sanitize(htmlRecu);
- N'écrivez jamais votre propre filtre avec des expressions régulières : il existe d'innombrables façons de le contourner.
-
Autres pièges à éviter
eval(),new Function()etsetTimeout("code", ...)avec une chaîne : ils exécutent du texte comme du code. On passe toujours une fonction:setTimeout(() => afficher(), 1000).- Données secrètes dans le code: tout le JavaScript est visible (Ctrl + U, DevTools). Jamais de mot de passe, de clé d'API privée ou de clé secrète dans un fichier
.js. - Jetons de connexion dans le
localStorage: un script injecté peut les lire. Les cookiesHttpOnlydéfinis par le serveur sont inaccessibles à JavaScript. - Validation uniquement côté client: elle améliore le confort, mais un attaquant peut l'ignorer (DevTools, requête directe). Le serveur doit toujours revérifier.
- Scripts externes: un script chargé depuis un CDN a tous les droits sur votre page. L'attribut
integrity(SRI) vérifie que le fichier n'a pas été modifié :
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.9/dist/js/bootstrap.bundle.min.js" integrity="sha384-…empreinte fournie par le site de Bootstrap…" crossorigin="anonymous" defer></script> -
La Content Security Policy (CSP)
- La CSP est une règle envoyée par le serveur (en-tête HTTP) qui indique au navigateur quels scripts il a le droit d'exécuter. Même si une faille XSS existe, une CSP stricte bloque le code injecté.
- Exemple d'en-tête : autoriser seulement les scripts du site et de jsDelivr, interdire les scripts dans les attributs :
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.jsdelivr.net; object-src 'none'- Avec cette règle, les attributs
onclicketonerroret les scripts internes sont bloqués : une raison de plus d'écrire tout son JavaScript dans des fichiers externes avecaddEventListener. - On peut aussi la tester dans une page avec une balise
<meta http-equiv="Content-Security-Policy" content="...">.
-
Liste de contrôle sécurité
-
Activités
| Question | Bonne pratique |
|---|---|
| J'affiche une donnée extérieure ? | textContent ou createElement |
| J'ai besoin de HTML venant de l'extérieur ? | DOMPurify.sanitize() |
| J'utilise une adresse fournie ? | Vérifier que le protocole est http: ou https: |
| J'exécute du texte comme du code ? | Jamais : pas de eval, pas de setTimeout avec une chaîne |
| Je valide un formulaire ? | Côté client pour le confort, côté serveur pour la sécurité |
| Je charge un script externe ? | Source de confiance, version fixée, attribut integrity |
| J'ai un secret dans mon code ? | Le déplacer côté serveur |
Activité 1 : observer une faille
- Créez une page avec un champ et un bouton « Publier » qui ajoute le message dans une liste avec
innerHTML. - Publiez
Bonjour <b>à tous</b>, puis<img src="x" onerror="document.body.style.background='red'">. Décrivez ce qui se passe. - Corrigez avec
textContentet republiez les deux messages. Que voit-on maintenant ?
Activité 2 : trouver les lignes dangereuses
const params = new URLSearchParams(location.search);
const nom = params.get("nom");
document.querySelector("#titre").innerHTML = "Bonjour " + nom;
document.querySelector("#pseudo").textContent = nom;
document.querySelector("#site").href = params.get("site");
setTimeout("verifierSession()", 5000);
const cleApi = "sk_live_51H8…";
- Pour chaque ligne, indiquez si elle est sûre ou dangereuse, expliquez pourquoi et proposez une correction.
Activité 3 : une carte de commentaire sûre
- Utilisez la fonction
creerCarteCommentairede la leçon pour afficher un tableau de 3 commentaires, dont un contient une balise piégée. Vérifiez dans l'inspecteur que la balise apparaît comme du texte.
Activité 4 : tester une CSP
- Ajoutez à la page de l'activité 1 (version non corrigée) la balise
<meta http-equiv="Content-Security-Policy" content="script-src 'self'">, avec votre code dans un fichier externe. Publiez de nouveau le message piégé : que signale la console ? - Déposez les trois versions dans le dépôt GitHub
js-securite-xss.
Corrigé de l'activité 2. Ligne 3 : dangereuse (XSS basée sur le DOM avec
Activité 4 : la console affiche un message de violation de la CSP (Executing inline event handler violates the following Content Security Policy directive…) et le code de
?nom=<img src=x onerror=...>) → utiliser textContent. Ligne 4 : sûre. Ligne 5 : dangereuse (?site=javascript:...) → vérifier le protocole avec new URL(). Ligne 6 : dangereuse (code sous forme de chaîne) → setTimeout(verifierSession, 5000). Ligne 7 : dangereuse (clé secrète visible par tous) → la garder sur le serveur.Activité 4 : la console affiche un message de violation de la CSP (Executing inline event handler violates the following Content Security Policy directive…) et le code de
onerror n'est pas exécuté.À retenir
- Toute donnée extérieure est non fiable.
innerHTML+ donnée non fiable = faille XSS; on utilisetextContentetcreateElement.- On vérifie les URL, on bannit
eval, on ne met aucun secret dans le JavaScript. - La sécurité réelle se fait aussi côté serveur; la CSP ajoute une protection supplémentaire.
Sources : MDN Web Docs (developer.mozilla.org), OWASP (owasp.org), documentations d'ESLint, Prettier et Vitest.