Leçon 10 / 20

Prévenir les attaques XSS

25 min PHP 8 — architecture, tests et sécurité Gratuit

Objectif : Afficher les champs de contact sans interpréter leur contenu comme du HTML exécutable.

Un nom de contact peut contenir du texte inattendu, qu’il provienne d’un formulaire ou d’une donnée déjà enregistrée. Si votre page l’insère directement dans du HTML, un navigateur peut interpréter ce contenu comme du balisage ou du code actif. Vous allez protéger le rendu du carnet en encodant les valeurs au moment où vous les affichez.

Cette attaque est appelée XSS, pour « script intersites » : une valeur contrôlée par un utilisateur est affichée dans une page et interprétée par le navigateur. Dans le carnet, les champs concernés incluent notamment le nom, le courriel, le téléphone et la recherche. Le risque porte sur le rendu, même si ces valeurs ont été validées par le service métier ou proviennent de la base de données.

Encoder à la sortie

Pour une valeur affichée dans du texte HTML ou dans un attribut HTML entre guillemets, utilisez htmlspecialchars avec l’encodage UTF-8 et les options ENT_QUOTES | ENT_SUBSTITUTE. Les caractères qui pourraient former du balisage sont alors représentés sous forme d’entités HTML : le navigateur affiche le texte au lieu de le traiter comme une balise. L’encodage se fait au dernier moment, dans la vue, sans réécrire la valeur enregistrée.

Rendre le carnet sans interpréter les contacts

Dans templates/carnet.php, préparez une opération d’encodage locale à la vue, puis utilisez-la à chaque emplacement contenant une donnée variable. L’exemple ci-dessous représente la page du carnet authentifié ; les variables sont fournies par le code qui rend le gabarit. Les valeurs destinées au texte, à la recherche, aux champs de formulaire et aux attributs sont toutes encodées avant leur insertion.

templates/carnet.phpPHP
<?php

declare(strict_types=1);

$echapper = static fn (string $valeur): string => htmlspecialchars(
    $valeur,
    ENT_QUOTES | ENT_SUBSTITUTE,
    'UTF-8'
);

?>
<!doctype html>
<html lang="fr">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Carnet de contacts privé</title>
  <link rel="stylesheet" href="/style.css">
</head>
<body class="atelier-page">
  <header class="atelier-entete">
    <a class="atelier-entete__titre" href="/">Carnet de contacts</a>
    <nav class="atelier-navigation" aria-label="Navigation principale">
      <a class="atelier-navigation__lien" href="/">Mes contacts</a>
      <form class="atelier-deconnexion" method="post" action="/deconnexion">
        <input type="hidden" name="jeton_csrf" value="<?= $echapper($jetonCsrf) ?>">
        <button class="atelier-bouton atelier-bouton--secondaire" type="submit">Se déconnecter</button>
      </form>
    </nav>
  </header>
  <main class="atelier-contenu">
    <section class="atelier-carnet" aria-labelledby="titre-carnet">
      <h1 id="titre-carnet">Mes contacts</h1>
      <p class="atelier-message" role="status"><?= $echapper($message) ?></p>
      <form class="atelier-recherche" method="get" action="/">
        <label class="atelier-formulaire__libelle" for="recherche">Rechercher un contact</label>
        <input class="atelier-formulaire__champ" id="recherche" name="recherche" type="search" value="<?= $echapper($recherche) ?>">
        <button class="atelier-bouton" type="submit">Rechercher</button>
      </form>
      <section class="atelier-contact-formulaire" aria-labelledby="titre-ajout">
        <h2 id="titre-ajout">Ajouter ou modifier un contact</h2>
        <form class="atelier-formulaire" method="post" action="/contacts">
          <input type="hidden" name="jeton_csrf" value="<?= $echapper($jetonCsrf) ?>">
          <input type="hidden" name="id" value="<?= $echapper((string) ($id ?? '')) ?>">
          <label class="atelier-formulaire__libelle" for="nom">Nom</label>
          <input class="atelier-formulaire__champ" id="nom" name="nom" autocomplete="name" value="<?= $echapper($nom ?? '') ?>" required>
          <label class="atelier-formulaire__libelle" for="courriel">Courriel</label>
          <input class="atelier-formulaire__champ" id="courriel" name="courriel" type="email" autocomplete="email" value="<?= $echapper($courriel ?? '') ?>" required>
          <label class="atelier-formulaire__libelle" for="telephone">Téléphone</label>
          <input class="atelier-formulaire__champ" id="telephone" name="telephone" type="tel" autocomplete="tel" value="<?= $echapper($telephone ?? '') ?>">
          <button class="atelier-bouton" type="submit">Enregistrer le contact</button>
        </form>
      </section>
      <section class="atelier-liste" aria-labelledby="titre-liste">
        <h2 id="titre-liste">Résultats</h2>
        <ul class="atelier-liste__contacts">
          <?php foreach ($contacts as $contact): ?>
            <li class="atelier-contact">
              <div class="atelier-contact__details">
                <h3 class="atelier-contact__nom"><?= $echapper($contact['nom']) ?></h3>
                <p class="atelier-contact__information"><?= $echapper($contact['courriel']) ?> · <?= $echapper($contact['telephone']) ?></p>
              </div>
              <div class="atelier-contact__actions">
                <a class="atelier-bouton atelier-bouton--secondaire" href="/contacts/<?= $echapper((string) $contact['id']) ?>">Modifier</a>
                <form method="post" action="/contacts/supprimer">
                  <input type="hidden" name="jeton_csrf" value="<?= $echapper($jetonCsrf) ?>">
                  <input type="hidden" name="id" value="<?= $echapper((string) $contact['id']) ?>">
                  <button class="atelier-bouton atelier-bouton--danger" type="submit">Supprimer</button>
                </form>
              </div>
            </li>
          <?php endforeach; ?>
        </ul>
      </section>
    </section>
  </main>
  <footer class="atelier-pied">
    <p>Carnet privé — accès réservé</p>
  </footer>
</body>
</html>

Le gabarit conserve les classes du squelette et ne transforme pas les valeurs affichées en fragments HTML. La chaîne passée à htmlspecialchars doit être de type texte ; les champs du contact et les variables de formulaire sont donc préparés comme des chaînes par le code qui rend la vue. L’identifiant est converti explicitement en chaîne avant encodage, y compris dans l’URL de modification.

Ne pas encoder trop tôt ni trop tard

Conservez les données dans leur forme normale dans le service et dans la base ; l’encodage HTML est une transformation de présentation. Si vous encodez à l’enregistrement, vous risquez de stocker des entités HTML, puis de les encoder une seconde fois à l’affichage : l’utilisateur verra alors des séquences telles que &lt;. À l’inverse, échapper un texte dans un attribut HTML ne rend pas automatiquement cette valeur sûre pour un autre contexte, comme du JavaScript ou une URL ayant un schéma dangereux.

Dans cette vue, les valeurs variables sont placées dans du texte HTML ou des attributs entre guillemets : l’encodage choisi répond à ces contextes. N’insérez pas de données utilisateur dans un bloc script, du CSS, ou un attribut d’événement tel que onclick. Si une valeur doit un jour être produite dans un autre contexte, choisissez une protection adaptée à ce contexte au lieu de réutiliser mécaniquement l’échappement HTML.

À retenir

  • Une donnée enregistrée n’est pas automatiquement sûre à afficher dans une page HTML.
  • Encodez chaque valeur au rendu avec `htmlspecialchars`, `ENT_QUOTES | ENT_SUBSTITUTE` et `UTF-8`.
  • Appliquez la protection au texte et aux attributs HTML contenant des données variables.
  • Gardez les données normales en base et adaptez la protection au contexte de sortie.

La prochaine leçon ajoute un contrôle spécifique aux formulaires qui modifient le carnet : la protection contre les requêtes CSRF.

Envie d'aller plus loin ?

Les cours Enovatic sont gratuits et le resteront. Si vous cherchez un accompagnement encadré, nos formations professionnelles sont animées par les mêmes formateurs.