À propos de ce cours
Présentation du TP
- Chapitre : 23 – Bonnes pratiques, sécurité et performance.
- Durée conseillée : 4 heures.
- Prérequis : les 4 leçons du chapitre 23, Node.js (version LTS) installé, VS Code avec Live Server, ESLint et Prettier, un compte GitHub.
- Contexte : l'école vous confie la page « Catalogue des cours » écrite par un ancien stagiaire. Elle fonctionne… mais un audit rapide a révélé des failles de sécurité, des problèmes de performance et un code difficile à maintenir, sans aucun test. Vous devez réaliser l'audit complet, corriger chaque problème et prouver la qualité du résultat par des outils et des tests.

Objectifs :
- Détecter et corriger des failles XSS (stockée, réfléchie et basée sur le DOM).
- Optimiser des événements en rafale avec debounce et throttle.
- Refactoriser un script en modules clairs et faire respecter les règles avec ESLint et Prettier.
- Écrire des tests unitaires avec Vitest.
node_modules ne doit jamais être envoyé sur GitHub.Partie 1 Préparer le projet et le dépôt GitHub
- Créez sur GitHub un dépôt public
tp23-auditer-securiser-optimiseravec unREADME.md, clonez-le et ouvrez-le dans VS Code. - Créez les fichiers de départ ci-dessous sans rien modifier:
index.html:
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TP 23 – Catalogue des cours</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.9/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="css/style.css">
<script type="module" src="js/app.js"></script>
</head>
<body>
<div class="progress fixed-top rounded-0" role="progressbar" aria-label="Progression de lecture" style="height: 6px;">
<div class="progress-bar" id="progression" style="width: 0%"></div>
</div>
<main class="container py-5">
<h1 class="mb-4">Catalogue des cours</h1>
<section class="mb-5">
<label for="recherche" class="form-label">Rechercher un cours</label>
<input type="search" id="recherche" class="form-control mb-2" placeholder="Ex. : bootstrap">
<p class="small text-body-secondary">Nombre de filtrages : <strong id="compteur">0</strong></p>
<ul id="liste" class="list-group"></ul>
</section>
<section class="mb-5">
<h2 class="h4">Laisser un avis</h2>
<div class="mb-2">
<label for="auteur" class="form-label">Votre nom</label>
<input type="text" id="auteur" class="form-control">
</div>
<div class="mb-2">
<label for="message" class="form-label">Votre avis (200 caractères maximum)</label>
<textarea id="message" class="form-control" rows="3"></textarea>
</div>
<button type="button" id="btn-publier" class="btn btn-primary">Publier</button>
</section>
<section>
<h2 class="h4">Avis des stagiaires</h2>
<div id="avis"></div>
</section>
<div class="espace-lecture"></div>
</main>
</body>
</html>
css/style.css:
.espace-lecture {
height: 2000px;
}
js/app.js:
// Catalogue des cours : version à auditer
var cours = [
{ titre: "Bootstrap 5.3", niveau: "débutant", heures: 30 },
{ titre: "JavaScript moderne", niveau: "intermédiaire", heures: 60 },
{ titre: "Angular", niveau: "avancé", heures: 45 },
{ titre: "Flutter", niveau: "intermédiaire", heures: 50 },
{ titre: "UI/UX Design", niveau: "débutant", heures: 25 },
{ titre: "React", niveau: "avancé", heures: 40 },
{ titre: "HTML et CSS", niveau: "débutant", heures: 35 },
{ titre: "Node.js", niveau: "avancé", heures: 30 }
]
var nbFiltrages = 0
function f(t) {
nbFiltrages++
document.getElementById("compteur").innerHTML = nbFiltrages
var r = []
for (var i = 0; i < cours.length; i++) {
if (cours[i].titre.toLowerCase().indexOf(t.toLowerCase()) != -1) r.push(cours[i])
}
var h = ""
for (var i = 0; i < r.length; i++) {
h = h + "<li class='list-group-item'>" + r[i].titre + " - " + r[i].heures + " h</li>"
}
if (r.length == 0) h = "<li class='list-group-item'>Aucun résultat pour " + t + "</li>"
document.getElementById("liste").innerHTML = h
}
document.getElementById("recherche").addEventListener("input", function () {
f(this.value)
})
document.getElementById("btn-publier").addEventListener("click", function () {
var a = document.getElementById("auteur").value
var m = document.getElementById("message").value
if (a == "" || m == "") {
alert("Tous les champs sont obligatoires")
return
}
if (m.length > 200) {
alert("Message trop long")
return
}
document.getElementById("avis").innerHTML += "<div class='card mb-2'><div class='card-body'><h5>" + a + "</h5><p class='mb-0'>" + m + "</p></div></div>"
localStorage.setItem("avis", document.getElementById("avis").innerHTML)
document.getElementById("auteur").value = ""
document.getElementById("message").value = ""
})
if (localStorage.getItem("avis")) document.getElementById("avis").innerHTML = localStorage.getItem("avis")
var q = new URLSearchParams(location.search).get("q")
if (q) {
document.getElementById("recherche").value = q
f(q)
} else {
f("")
}
window.addEventListener("scroll", function () {
console.log("scroll")
var p = window.scrollY / (document.documentElement.scrollHeight - window.innerHeight) * 100
document.getElementById("progression").style.width = p + "%"
})
setTimeout("console.log('Page prête')", 1000)
- Initialisez npm et installez les outils (voir les leçons sur le code propre et les tests) :
npm init -y
npm pkg set type=module
npm install --save-dev eslint @eslint/js globals prettier eslint-config-prettier vitest
npm pkg set scripts.lint="eslint ." scripts.format="prettier --write ." scripts.test="vitest"
- Créez
.gitignore(node_modules/etcoverage/), puiseslint.config.js,.prettierrcet.prettierignoreen reprenant exactement la configuration de la leçon « Écrire un code propre et maintenable ». - Lancez la page avec Live Server (le script est un module : il ne fonctionne pas en
file://) et vérifiez que les 8 cours s'affichent.
Commit : git add . puis git commit -m "Partie 1 : projet de départ et outils" et git push.
Partie 2 Réaliser l'audit
Avant de corriger quoi que ce soit, on mesure. Créez dans le README.md une section « Audit » et notez le résultat de chaque test.
2.1 – Sécurité. Utilisez la charge de test suivante (elle colore la page en rouge si elle s'exécute) :
<img src="x" onerror="document.body.style.background='red'">
- Test S1 : collez-la dans le champ de recherche.
- Test S2 : publiez un avis avec le nom « Test » et cette charge comme message.
- Test S3 : rechargez la page (F5) après le test S2. Expliquez pourquoi la page redevient rouge.
- Test S4 : ouvrez
index.html?q=suivi de la charge (le navigateur encode l'adresse automatiquement). - Pour chaque test, indiquez le type de XSS (stockée, réfléchie, basée sur le DOM) et la ligne du code responsable. Effacez ensuite le stockage : DevTools → Application → Local Storage → clic droit → Clear.
2.2 – Performance.
- Test P1 : tapez « bootstrap » dans la recherche et relevez le nombre de filtrages affiché.
- Test P2 : faites défiler la page de haut en bas et observez la console : combien de messages « scroll » s'affichent ?
2.3 – Qualité.
- Test Q1 : lancez
npm run lint. Relevez le nombre d'erreurs et d'avertissements, et classez-les par règle. - Test Q2 : listez les défauts que l'outil ne signale pas (nombre magique, duplication, mélange calcul/affichage, stockage de HTML…).
Présentez votre audit dans ce tableau :
| N° | Catégorie | Problème | Ligne(s) | Gravité (haute, moyenne, faible) | Correction prévue |
|---|---|---|---|---|---|
| 1 | Sécurité | ||||
| 2 | … |
captures/xss.png) et de la sortie d'ESLint (captures/eslint-avant.png).Commit : git commit -am "Partie 2 : audit".
Partie 3 Séparer la logique dans des modules
Pour pouvoir corriger proprement et tester, on sépare le code en trois fichiers :
| Fichier | Contenu | Accès au DOM ? |
|---|---|---|
js/catalogue.js | Les données et les fonctions pures du métier | Non |
js/outils.js | debounce et throttle | Non |
js/app.js | Lecture des champs, affichage, événements | Oui |
Dans js/catalogue.js, écrivez et exportez :
COURS: le tableau des 8 cours, etLONGUEUR_MAX_AVIS = 200.filtrerCours(listeCours, texteRecherche): renvoie les cours dont le titre contient le texte, sans tenir compte des majuscules ni des espaces autour.validerAvis(auteur, message): renvoie{ valide: true, erreur: "" }ou{ valide: false, erreur: "…" }(champ vide ou message de plus de 200 caractères).calculerProgression(positionDefilement, hauteurPage, hauteurFenetre): renvoie un pourcentage entre 0 et 100 (100 si la page ne défile pas).
Dans js/app.js, importez ces fonctions (import { COURS, filtrerCours } from "./catalogue.js";) et ne gardez que ce qui concerne la page. Récupérez chaque élément du DOM une seule fois dans une constante.
Commit : git commit -am "Partie 3 : séparation en modules".
Partie 4 Corriger les failles de sécurité
- Affichez la liste des cours et le message « Aucun résultat pour … » avec
document.createElementettextContent. La méthodereplaceChildren()remplace tout le contenu d'un élément. - Affichez les avis avec une fonction
creerCarteAvis({ auteur, message })qui construit la carte élément par élément. - Enregistrez dans le
localStorageun tableau d'objets en JSON (JSON.stringify), et non plus du HTML. À la lecture, protégezJSON.parseavec un bloctry...catch(données éventuellement corrompues). - Utilisez
validerAvisavant de publier. - Remplacez
setTimeout("…", 1000)par une fonction, ou supprimez la ligne si elle est inutile. - Rejouez les tests S1 à S4 : la charge doit s'afficher comme du texte et la page ne doit plus devenir rouge.
innerHTML, insertAdjacentHTML et eval sous toutes leurs formes. La règle no-restricted-syntax de votre configuration ESLint doit le confirmer.Commit : git commit -am "Partie 4 : correction des failles XSS".
Partie 5 Optimiser les performances
- Dans
js/outils.js, écrivez et exportezdebounce(fonction, delai)etthrottle(fonction, intervalle)(voir la leçon). - Appliquez un debounce de 300 ms à la recherche. Rejouez le test P1 : le compteur doit passer à 1 filtrage pour « bootstrap » tapé d'un trait.
- Appliquez un throttle de 100 ms à la mise à jour de la barre de progression, avec un écouteur passif (
{ passive: true }). Supprimez leconsole.log. - Vérifiez que la barre atteint bien 100 % en bas de la page. Si ce n'est pas le cas, améliorez
throttlepour qu'il exécute un dernier appel à la fin de la rafale. - Notez dans le README les nouvelles mesures P1 et P2 à côté des anciennes.
Commit : git commit -am "Partie 5 : debounce et throttle".
Partie 6 Atteindre zéro problème ESLint
- Lancez
npm run lint, puis corrigez toutes les erreurs et tous les avertissements :const/let,===, accolades, noms explicites, plus deconsole.log… - Remplacez les nombres magiques par des constantes nommées (
DELAI_RECHERCHE,INTERVALLE_DEFILEMENT,CLE_STOCKAGE…). - Lancez
npm run format. - Objectif :
npm run lintn'affiche plus aucune ligne. Faites une capture (captures/eslint-apres.png).
Commit : git commit -am "Partie 6 : 0 problème ESLint".
Partie 7 Écrire les tests unitaires
Créez le dossier tests et complétez ce fichier tests/catalogue.test.js:
import { describe, it, expect } from "vitest";
import {
COURS,
LONGUEUR_MAX_AVIS,
filtrerCours,
validerAvis,
calculerProgression,
} from "../js/catalogue.js";
describe("filtrerCours", () => {
it("trouve un cours sans tenir compte des majuscules", () => {
const resultats = filtrerCours(COURS, "BOOTSTRAP");
expect(resultats).toHaveLength(1);
expect(resultats[0].titre).toBe("Bootstrap 5.3");
});
// À compléter : recherche vide, espaces autour du texte, aucun résultat
});
describe("validerAvis", () => {
// À compléter : avis correct, champs vides (it.each), exactement 200 caractères, 201 caractères
});
describe("calculerProgression", () => {
// À compléter : haut de page, milieu, au-delà du bas, page qui ne défile pas
});
- Tant qu'un bloc
describeest vide, Vitest affiche No test found in suite: c'est normal, complétez-le. - Écrivez au moins 12 tests au total, dont des tests de valeurs limites (200 et 201 caractères).
- Bonus : testez
debouncedanstests/outils.test.jsavec les minuteurs simulés de Vitest (vi.useFakeTimers(),vi.advanceTimersByTime(),vi.fn()). - Lancez
npx vitest run: tous les tests doivent passer. Faites une capture (captures/tests.png). - Prouvez l'utilité des tests : remplacez
>par>=dans la vérification de longueur devaliderAvis, relancez les tests et recopiez le message d'échec dans le README. Remettez le code correct.
Commit : git commit -am "Partie 7 : tests unitaires".
Partie 8 Aller plus loin (bonus)
- Content Security Policy : ajoutez dans le
<head>la balise suivante, déplacez les attributsstyle="…"du HTML versstyle.css, puis vérifiez dans la console qu'aucune violation n'apparaît :
<meta http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'; style-src 'self' https://cdn.jsdelivr.net; img-src 'self' data:; object-src 'none'">
- Preuve de la CSP : remettez temporairement l'ancien
innerHTMLpour les avis et republiez la charge de test : quel message affiche la console ? La page devient-elle rouge ? - Accessibilité et retour utilisateur : remplacez les
alert()par un message Bootstrap (alert alert-danger) affiché sous le formulaire, avectextContent. - Audit Lighthouse : dans les DevTools, onglet Lighthouse, générez un rapport « Bonnes pratiques » et « Accessibilité » avant et après vos corrections. Comparez les scores.
Commit : git commit -am "Partie 8 : bonus".
Livrables et barème
- Le lien du dépôt GitHub
tp23-auditer-securiser-optimiser, avec l'historique des commits. - Le
README.md: tableau d'audit, mesures avant/après, message d'échec du test volontaire. - Le dossier
captures.
| Critère | Points |
|---|---|
Partie 1 : projet, outils installés, .gitignore correct | 1 |
| Partie 2 : audit complet et argumenté (sécurité, performance, qualité) | 4 |
| Partie 3 : séparation en modules, fonctions pures | 3 |
| Partie 4 : les 4 failles corrigées, stockage en JSON, tests S1 à S4 négatifs | 4 |
| Partie 5 : debounce et throttle fonctionnels, mesures avant/après | 3 |
| Partie 6 : 0 problème ESLint, code formaté, constantes nommées | 2 |
| Partie 7 : au moins 12 tests pertinents qui passent | 3 |
| Total | 20 |
Bonus (partie 8 et tests de debounce) | +2 |
innerHTML alimenté par une donnée utilisateur ne peut pas obtenir la moyenne à la partie concernée.