Avez-vous une question? (216) 97 656 803 hajjriadh@gmail.com
JavaScript

TP 23 : Auditer, sécuriser et optimiser un script

TP 23 du cours JavaScript : auditez, sécurisez et optimisez la page « Catalogue des cours ». Vous détectez quatre failles XSS, mesurez les problèmes de performance et de qualité, séparez le code en mo...

0 chapitres 0 QCM Accès gratuit

À propos de ce cours

TP 23 du cours JavaScript : auditez, sécurisez et optimisez la page « Catalogue des cours ». Vous détectez quatre failles XSS, mesurez les problèmes de performance et de qualité, séparez le code en modules, remplacez innerHTML par textContent et createElement, ajoutez debounce et throttle, atteignez zéro problème ESLint avec Prettier et écrivez des tests unitaires avec Vitest. Avec barème, bonus CSP et audit Lighthouse.

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.

TP 23 : démarche d'audit, structure du projet et outils

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.
Règle du TP : un commit à la fin de chaque partie (au minimum 8 commits), avec un message clair. Le dossier node_modules ne doit jamais être envoyé sur GitHub.
Partie 1 Préparer le projet et le dépôt GitHub
  1. Créez sur GitHub un dépôt public tp23-auditer-securiser-optimiser avec un README.md, clonez-le et ouvrez-le dans VS Code.
  2. 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)
  1. 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"
  1. Créez .gitignore (node_modules/ et coverage/), puis eslint.config.js, .prettierrc et .prettierignore en reprenant exactement la configuration de la leçon « Écrire un code propre et maintenable ».
  2. 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'">
  1. Test S1 : collez-la dans le champ de recherche.
  2. Test S2 : publiez un avis avec le nom « Test » et cette charge comme message.
  3. Test S3 : rechargez la page (F5) après le test S2. Expliquez pourquoi la page redevient rouge.
  4. Test S4 : ouvrez index.html?q= suivi de la charge (le navigateur encode l'adresse automatiquement).
  5. 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.

  1. Test P1 : tapez « bootstrap » dans la recherche et relevez le nombre de filtrages affiché.
  2. Test P2 : faites défiler la page de haut en bas et observez la console : combien de messages « scroll » s'affichent ?

2.3 – Qualité.

  1. Test Q1 : lancez npm run lint. Relevez le nombre d'erreurs et d'avertissements, et classez-les par règle.
  2. 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égorieProblèmeLigne(s)Gravité (haute, moyenne, faible)Correction prévue
1Sécurité
2…
Attendu : au moins 4 failles de sécurité, 2 problèmes de performance et 8 problèmes de qualité. Faites une capture de la page rouge (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 :

FichierContenuAccès au DOM ?
js/catalogue.jsLes données et les fonctions pures du métierNon
js/outils.jsdebounce et throttleNon
js/app.jsLecture des champs, affichage, événementsOui

Dans js/catalogue.js, écrivez et exportez :

  • COURS: le tableau des 8 cours, et LONGUEUR_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é
  1. Affichez la liste des cours et le message « Aucun résultat pour … » avec document.createElement et textContent. La méthode replaceChildren() remplace tout le contenu d'un élément.
  2. Affichez les avis avec une fonction creerCarteAvis({ auteur, message }) qui construit la carte élément par élément.
  3. Enregistrez dans le localStorage un tableau d'objets en JSON (JSON.stringify), et non plus du HTML. À la lecture, protégez JSON.parse avec un bloc try...catch (données éventuellement corrompues).
  4. Utilisez validerAvis avant de publier.
  5. Remplacez setTimeout("…", 1000) par une fonction, ou supprimez la ligne si elle est inutile.
  6. Rejouez les tests S1 à S4 : la charge doit s'afficher comme du texte et la page ne doit plus devenir rouge.
Interdit : 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
  1. Dans js/outils.js, écrivez et exportez debounce(fonction, delai) et throttle(fonction, intervalle) (voir la leçon).
  2. Appliquez un debounce de 300 ms à la recherche. Rejouez le test P1 : le compteur doit passer à 1 filtrage pour « bootstrap » tapé d'un trait.
  3. Appliquez un throttle de 100 ms à la mise à jour de la barre de progression, avec un écouteur passif ({ passive: true }). Supprimez le console.log.
  4. Vérifiez que la barre atteint bien 100 % en bas de la page. Si ce n'est pas le cas, améliorez throttle pour qu'il exécute un dernier appel à la fin de la rafale.
  5. 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
  1. Lancez npm run lint, puis corrigez toutes les erreurs et tous les avertissements : const/let, ===, accolades, noms explicites, plus de console.log…
  2. Remplacez les nombres magiques par des constantes nommées (DELAI_RECHERCHE, INTERVALLE_DEFILEMENT, CLE_STOCKAGE…).
  3. Lancez npm run format.
  4. Objectif : npm run lint n'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
});
  1. Tant qu'un bloc describe est vide, Vitest affiche No test found in suite: c'est normal, complétez-le.
  2. Écrivez au moins 12 tests au total, dont des tests de valeurs limites (200 et 201 caractères).
  3. Bonus : testez debounce dans tests/outils.test.js avec les minuteurs simulés de Vitest (vi.useFakeTimers(), vi.advanceTimersByTime(), vi.fn()).
  4. Lancez npx vitest run: tous les tests doivent passer. Faites une capture (captures/tests.png).
  5. Prouvez l'utilité des tests : remplacez > par >= dans la vérification de longueur de validerAvis, 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)
  1. Content Security Policy : ajoutez dans le <head> la balise suivante, déplacez les attributs style="…" du HTML vers style.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'">
  1. Preuve de la CSP : remettez temporairement l'ancien innerHTML pour les avis et republiez la charge de test : quel message affiche la console ? La page devient-elle rouge ?
  2. Accessibilité et retour utilisateur : remplacez les alert() par un message Bootstrap (alert alert-danger) affiché sous le formulaire, avec textContent.
  3. 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èrePoints
Partie 1 : projet, outils installés, .gitignore correct1
Partie 2 : audit complet et argumenté (sécurité, performance, qualité)4
Partie 3 : séparation en modules, fonctions pures3
Partie 4 : les 4 failles corrigées, stockage en JSON, tests S1 à S4 négatifs4
Partie 5 : debounce et throttle fonctionnels, mesures avant/après3
Partie 6 : 0 problème ESLint, code formaté, constantes nommées2
Partie 7 : au moins 12 tests pertinents qui passent3
Total20
Bonus (partie 8 et tests de debounce)+2
Rappel : un projet dont les tests ne passent pas ou qui contient encore un innerHTML alimenté par une donnée utilisateur ne peut pas obtenir la moyenne à la partie concernée.