Aller au contenu
Réussir mon BTS SIO

Tape une notion, une commande ou le nom d'une épreuve.

Par exemple

Chapitre 1622 minIntermédiaire

Exercices PHP corrigés type E6 et E7

Par Josselyn C., prof de BTS SIO. Mis à jour le .

À la fin de ce chapitre, tu sauras

  • répondre à une question de code d'E7 en quatre temps ;
  • auditer une page PHP et classer ses failles ;
  • réaliser une évolution complète comme à l'E6 ;
  • repérer les erreurs classiques en quelques secondes.

10 exercices corrigés

Ce chapitre rassemble des exercices PHP corrigés construits comme les épreuves du BTS SIO SLAM : un cas d'audit de code dans l'esprit de l'écrit E7 (trouver les failles d'un extrait numéroté, les nommer, les corriger), un cas d'évolution dans l'esprit de l'heure de réalisation de l'E6, et une série de « trouve l'erreur » à faire en quelques minutes. Tous reprennent l'application Prêtmat du cours ; chaque corrigé renvoie au chapitre qui explique la notion.

Comment répondre à une question de code

À l'écrit E7, une question de code se traite en quatre temps, et chacun rapporte des points (rédiger une réponse qui rapporte des points) :

  1. Situer : le fichier et les numéros de ligne concernés.
  2. Nommer : la faille, avec son nom exact (injection SQL, XSS stockée, CSRF, contrôle d'accès défaillant, IDOR).
  3. Expliquer : d'où vient la donnée, comment elle est utilisée, ce qu'un attaquant peut obtenir.
  4. Corriger : les lignes réécrites, et rien qu'elles, en indiquant leurs numéros.

Sur papier, écris un code lisible : indentation, noms de variables du sujet, une instruction par ligne. Tu n'as pas d'ordinateur pour vérifier : relis chaque ;, chaque parenthèse et chaque guillemet (lire et écrire du code sur papier).

Cas type E7 : l'audit d'une page de Prêtmat

La société a ajouté à Prêtmat la réservation de matériel : un employé réserve un vidéoprojecteur pour une date future. La table reservation contient id, id_materiel, id_utilisateur, date_debut, date_fin et un commentaire saisi par l'employé. Un stagiaire a écrit la page qui affiche les réservations d'un employé et permet de les annuler. Avant la mise en production, le responsable informatique te demande un audit.

text
 1  <?php
 2  session_start();
 3  require 'bdd.php';
 4  $id = $_GET['id_utilisateur'];
 5  if (isset($_GET['annuler'])) {
 6      $pdo->exec('DELETE FROM reservation WHERE id = ' . $_GET['annuler']);
 7  }
 8  $sql = "SELECT r.id, m.reference, r.date_debut, r.commentaire
 9          FROM reservation r JOIN materiel m ON m.id = r.id_materiel
10          WHERE r.id_utilisateur = $id";
11  $reservations = $pdo->query($sql)->fetchAll();
12  ?>
13  <h1>Réservations de <?= $_SESSION['nom'] ?></h1>
14  <?php foreach ($reservations as $r): ?>
15    <p><?= $r['reference'] ?> le <?= $r['date_debut'] ?> : <?= $r['commentaire'] ?>
16       <a href="?id_utilisateur=<?= $id ?>&annuler=<?= $r['id'] ?>">Annuler</a></p>
17  <?php endforeach; ?>

Exercice 1 sur 10 Question 1 : l'accès à la page Réussi À revoir

Un employé connecté est sur sa page reservations.php?id_utilisateur=2. Que se passe-t-il s'il remplace 2 par 3 ? Et si une personne non connectée ouvre cette adresse ? Nomme la faille et indique les lignes en cause.

Voir la réponseMasquer la réponse de l'exercice 1

Lignes 2, 4 et 10. L'identifiant de l'employé vient de l'URL (ligne 4) et sert directement de critère (ligne 10). En le changeant, l'employé voit les réservations de l'employé 3, et peut essayer tous les numéros. C'est une référence directe non sécurisée à un objet (IDOR), une forme de contrôle d'accès défaillant (A01 du Top 10 de l'OWASP). Et rien ne vérifie que l'utilisateur est connecté : la ligne 2 démarre la session mais ne la teste jamais, donc une personne non connectée voit aussi les réservations.

Correction : l'identifiant se lit dans la session, après avoir vérifié la connexion.

text
4  $utilisateur = exigerConnexion();       // redirige vers la connexion sinon
4' $id = $utilisateur['id'];               // jamais l'URL

Tu avais trouvé ?

Exercice 2 sur 10 Question 2 : la requête de la ligne 10 Réussi À revoir

Identifie la vulnérabilité de la requête des lignes 8 à 10, explique son mécanisme et réécris les lignes 8 à 11.

Voir la réponseMasquer la réponse de l'exercice 2

Injection SQL, lignes 4, 10 et 11. $id vient de l'URL et est concaténé dans le texte de la requête, exécutée par query(). Une valeur qui n'est pas un nombre, mais un morceau de SQL, modifie la requête : elle peut rendre la condition toujours vraie (toutes les réservations de tous les employés) ou ajouter un UNION SELECT qui lit d'autres tables, comme utilisateur et ses empreintes de mots de passe.

text
 8  $stmt = $pdo->prepare('SELECT r.id, m.reference, r.date_debut, r.commentaire
 9      FROM reservation r JOIN materiel m ON m.id = r.id_materiel
10      WHERE r.id_utilisateur = :id ORDER BY r.date_debut');
11  $stmt->execute(['id' => $utilisateur['id']]);
11' $reservations = $stmt->fetchAll();

Avec l'identifiant lu en session (question 1), la valeur n'est plus une saisie ; la requête préparée reste la règle, pour toute valeur (chapitre 7).

Tu avais trouvé ?

Exercice 3 sur 10 Question 3 : l'annulation Réussi À revoir

Relève quatre problèmes dans l'annulation d'une réservation (lignes 5, 6 et 16) et propose une version corrigée, côté traitement et côté affichage.

Voir la réponseMasquer la réponse de l'exercice 3
  1. Injection SQL, ligne 6 : $_GET['annuler'] est concaténé dans un DELETE. Une condition injectée peut supprimer toutes les réservations.
  2. Contrôle d'accès, ligne 6 : le DELETE ne vérifie pas que la réservation appartient à l'utilisateur. N'importe qui peut annuler les réservations des autres en changeant le numéro.
  3. Modification en GET et CSRF, lignes 5 et 16 : l'annulation se fait par un simple lien. Une page piégée, une image dont l'adresse pointe vers ce lien, ou un robot qui suit les liens, déclenche l'annulation au nom de l'employé connecté, sans jeton pour l'en empêcher.
  4. Pas de redirection après l'action : actualiser la page relance l'annulation, et l'adresse avec annuler= reste dans l'historique.

Traitement corrigé, en tête de page :

php
<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    verifierCsrf();
    $idReservation = filter_input(INPUT_POST, 'annuler', FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]]);
    if (is_int($idReservation)) {
        $stmt = $pdo->prepare('DELETE FROM reservation WHERE id = :id AND id_utilisateur = :utilisateur');
        $stmt->execute(['id' => $idReservation, 'utilisateur' => $utilisateur['id']]);
        flash($stmt->rowCount() === 1 ? 'succes' : 'erreur',
              $stmt->rowCount() === 1 ? 'Réservation annulée.' : 'Réservation introuvable.');
    }
    header('Location: /reservations.php', true, 303);
    exit;
}

Affichage corrigé de la ligne 16, un formulaire POST avec le jeton :

php
<form method="post">
  <?= champCsrf() ?>
  <button name="annuler" value="<?= e($r['id']) ?>">Annuler</button>
</form>

La condition AND id_utilisateur = :utilisateur règle le contrôle d'accès dans la requête elle-même : une réservation d'un autre employé n'est tout simplement pas trouvée.

Tu avais trouvé ?

Exercice 4 sur 10 Question 4 : l'affichage Réussi À revoir

Lignes 13 à 16, quelles données sont affichées sans protection ? Pour chacune, dis d'où elle vient et classe le risque. Corrige les lignes 13 et 15.

Voir la réponseMasquer la réponse de l'exercice 4
  • $r['commentaire'] (ligne 15) : saisi par un employé et enregistré en base, puis affiché à chaque consultation. C'est une XSS stockée, la plus grave ici : un commentaire contenant du script s'exécute dans le navigateur de quiconque affiche la page, y compris le gestionnaire.
  • $id (ligne 16) : vient de l'URL et est réinséré dans la page, dans un attribut href. C'est une XSS réfléchie (et la question 1 supprime déjà ce paramètre).
  • $r['reference'] et $r['date_debut'] : viennent de la base, saisis par le gestionnaire ou produits par l'application ; risque faible, mais la règle est d'échapper toute donnée.
  • $_SESSION['nom'] (ligne 13) : saisi par le gestionnaire à la création du compte ; risque faible, même règle.
text
13  <h1>Réservations de <?= e($utilisateur['nom']) ?></h1>
15    <p><?= e($r['reference']) ?> le <?= e($r['date_debut']) ?> : <?= e($r['commentaire']) ?></p>

e() est la fonction courte du chapitre 5, qui appelle htmlspecialchars().

Tu avais trouvé ?

Exercice 5 sur 10 Question 5 : la synthèse pour le responsable Réussi À revoir

Rédige en cinq lignes au plus la synthèse de ton audit pour le responsable informatique : les failles par ordre de gravité, et la mesure générale qui aurait évité la plupart d'entre elles.

Voir la réponseMasquer la réponse de l'exercice 5

« La page présente quatre vulnérabilités qui interdisent sa mise en production. Par ordre de gravité : deux injections SQL (lignes 6 et 10), qui exposent toute la base ; un contrôle d'accès défaillant (lignes 4 et 6), qui permet de consulter et d'annuler les réservations des autres employés sans même être connecté ; une XSS stockée par le commentaire (ligne 15) ; une CSRF sur l'annulation en GET (ligne 16). Les corrections sont simples : requêtes préparées, identifiant lu en session, échappement à l'affichage, annulation en POST avec jeton. Mesure générale : faire passer toutes les pages par le contrôleur frontal, qui vérifie la connexion et le jeton CSRF, et ajouter une revue de code avant chaque mise en production. »

Tu avais trouvé ?

Cas type E6 : ajouter la réservation de matériel

Pendant l'heure de réalisation de l'E6, la commission te demande de faire évoluer ton application : « Un employé doit pouvoir réserver un matériel pour une période future. Deux réservations du même matériel ne peuvent pas se chevaucher, et un matériel réformé ne peut pas être réservé. » (la phase 2 de l'E6).

Exercice 6 sur 10 Étape 1 : la table Réussi À revoir

Écris l'instruction SQL qui crée la table reservation avec ses contraintes.

Voir la réponseMasquer la réponse de l'exercice 6
sql
CREATE TABLE reservation (
  id INT AUTO_INCREMENT PRIMARY KEY,
  id_materiel INT NOT NULL,
  id_utilisateur INT NOT NULL,
  date_debut DATE NOT NULL,
  date_fin DATE NOT NULL,
  commentaire VARCHAR(255) NULL,
  CONSTRAINT fk_reservation_materiel FOREIGN KEY (id_materiel) REFERENCES materiel (id),
  CONSTRAINT fk_reservation_utilisateur FOREIGN KEY (id_utilisateur) REFERENCES utilisateur (id),
  CONSTRAINT ck_reservation_dates CHECK (date_fin >= date_debut),
  INDEX idx_reservation_materiel_dates (id_materiel, date_debut, date_fin)
);

La contrainte CHECK refuse une période à l'envers ; l'index accélère la recherche des chevauchements, faite à chaque réservation. Le non-chevauchement, lui, ne s'exprime pas par une contrainte simple dans MySQL : il se vérifie dans le code, dans une transaction.

Tu avais trouvé ?

Exercice 7 sur 10 Étape 2 : détecter un chevauchement Réussi À revoir

Une réservation existe déjà pour le vidéoprojecteur VP-0001 du 10 au 12 novembre 2026. Pour chaque demande, dis si elle chevauche la réservation existante : du 12 au 14 ; du 13 au 15 ; du 8 au 9 ; du 5 au 20 ; du 9 au 10. Écris ensuite la condition SQL qui détecte un chevauchement entre une période [:debut, :fin] et les réservations existantes.

Voir la réponseMasquer la réponse de l'exercice 7
DemandeChevauche le 10-12 ?Pourquoi
12 au 14OuiLe 12 est dans les deux périodes
13 au 15NonCommence après la fin de la réservation
8 au 9NonSe termine avant son début
5 au 20OuiEnglobe toute la réservation
9 au 10OuiLe 10 est dans les deux périodes

Deux périodes se chevauchent quand chacune commence avant que l'autre se termine :

sql
SELECT COUNT(*) FROM reservation
WHERE id_materiel = :materiel AND date_debut <= :fin AND date_fin >= :debut

Cette condition couvre tous les cas (chevauchement à gauche, à droite, inclusion, englobement) d'un seul coup. Les dates de fin sont incluses (on rend le matériel le soir) : d'où <= et >=. La vérifier à la main sur les cinq demandes du tableau est un bon réflexe avant de coder.

Tu avais trouvé ?

Exercice 8 sur 10 Étape 3 : la fonction de réservation Réussi À revoir

Écris la fonction reserver(PDO $pdo, int $idMateriel, int $idUtilisateur, DateTimeImmutable $debut, DateTimeImmutable $fin, ?string $commentaire): int, qui applique les deux règles de gestion et renvoie l'identifiant de la réservation créée. Deux employés qui réservent le même matériel au même instant ne doivent pas pouvoir obtenir des réservations qui se chevauchent.

Voir la réponseMasquer la réponse de l'exercice 8
php
<?php
declare(strict_types=1);

final class ReservationImpossible extends RuntimeException
{
}

function reserver(PDO $pdo, int $idMateriel, int $idUtilisateur,
                  DateTimeImmutable $debut, DateTimeImmutable $fin, ?string $commentaire): int
{
    $pdo->beginTransaction();
    try {
        // Verrou sur le matériel : deux réservations simultanées passent l'une après l'autre
        $stmt = $pdo->prepare('SELECT etat FROM materiel WHERE id = :id FOR UPDATE');
        $stmt->execute(['id' => $idMateriel]);
        $etat = $stmt->fetchColumn();
        if ($etat === false || $etat === 'reforme') {
            throw new ReservationImpossible('Ce matériel ne peut pas être réservé.');
        }

        $stmt = $pdo->prepare(
            'SELECT COUNT(*) FROM reservation
             WHERE id_materiel = :materiel AND date_debut <= :fin AND date_fin >= :debut'
        );
        $stmt->execute([
            'materiel' => $idMateriel,
            'debut' => $debut->format('Y-m-d'),
            'fin' => $fin->format('Y-m-d'),
        ]);
        if ((int) $stmt->fetchColumn() > 0) {
            throw new ReservationImpossible('Ce matériel est déjà réservé sur une partie de ces dates.');
        }

        $stmt = $pdo->prepare(
            'INSERT INTO reservation (id_materiel, id_utilisateur, date_debut, date_fin, commentaire)
             VALUES (:materiel, :utilisateur, :debut, :fin, :commentaire)'
        );
        $stmt->execute([
            'materiel' => $idMateriel,
            'utilisateur' => $idUtilisateur,
            'debut' => $debut->format('Y-m-d'),
            'fin' => $fin->format('Y-m-d'),
            'commentaire' => $commentaire,
        ]);
        $id = (int) $pdo->lastInsertId();

        $pdo->commit();
        return $id;
    } catch (Throwable $e) {
        $pdo->rollBack();
        throw $e;
    }
}

C'est la même structure que emprunter() (chapitre 8) : verrou FOR UPDATE sur la ligne du matériel, vérifications, insertion, validation. Sans le verrou, deux employés pourraient passer la vérification du chevauchement en même temps, puis insérer chacun leur réservation. Les dates (fin après début, début dans le futur) se valident avant l'appel, dans le contrôleur ; la contrainte CHECK de la table est le filet de sécurité.

Tu avais trouvé ?

Exercice 9 sur 10 Étape 4 : le contrôleur et la recette Réussi À revoir

Écris l'action du contrôleur qui traite le formulaire de réservation (route POST /materiels/{id}/reserver), puis liste les tests que tu montres à la commission pour prouver que l'évolution fonctionne.

Voir la réponseMasquer la réponse de l'exercice 9
php
<?php
public function reserver(int $id): void
{
    $utilisateur = exigerConnexion();     // le jeton CSRF est vérifié par le contrôleur frontal

    $aujourdhui = new DateTimeImmutable('today');
    $debut = lireDate($_POST, 'date_debut');
    $fin = lireDate($_POST, 'date_fin');
    $commentaire = mb_substr(texte($_POST, 'commentaire'), 0, 255);

    if ($debut === null || $fin === null || $debut < $aujourdhui || $fin < $debut) {
        flash('erreur', 'Choisis une période future, la fin après le début.');
        rediriger("/materiels/$id");
    }

    try {
        reserver(bdd(), $id, $utilisateur['id'], $debut, $fin, $commentaire === '' ? null : $commentaire);
        flash('succes', 'Réservation enregistrée.');
    } catch (ReservationImpossible $e) {
        flash('erreur', $e->getMessage());
    }
    rediriger("/materiels/$id");
}

Les tests à montrer (la recette) :

  1. Une réservation valide sur un matériel libre : message de succès, ligne en base.
  2. Une réservation qui chevauche une réservation existante (du 12 au 14 si le 10-12 existe) : refus avec le message.
  3. Une réservation accolée (du 13 au 15) : acceptée.
  4. Un matériel réformé (PC-0087) : refus.
  5. Une période à l'envers ou dans le passé : refus par la validation.
  6. Un commentaire contenant des balises HTML : affiché en texte sur la page des réservations.
  7. Un utilisateur non connecté qui envoie la requête : redirigé vers la connexion.

Annonce les tests avant de les jouer, et montre les cas limites (3 et 2) : c'est ce qui convainc la commission que tu as compris la règle, pas seulement écrit du code (la recette de l'E6).

Tu avais trouvé ?

Trouve l'erreur

Exercice 10 sur 10 Six erreurs classiques Réussi À revoir

Chaque extrait contient une erreur. Trouve-la et corrige-la.

php
<?php
// 1
if ($_SESSION['utilisateur']['role'] = 'gestionnaire') {
    afficherMenuGestion();
}

// 2
$stmt = $pdo->prepare('SELECT * FROM materiel WHERE id = :id');
$stmt->execute([':id', $id]);

// 3
if ($utilisateur['role'] !== 'gestionnaire') {
    header('Location: /materiels');
}
supprimerMateriel($pdo, $id);

// 4
$ok = password_verify(password_hash($motDePasse, PASSWORD_DEFAULT), $u['mot_de_passe']);

// 5
$stmt = $pdo->prepare('SELECT reference FROM materiel ORDER BY reference');
$stmt->execute();
foreach ($stmt->fetch() as $m) {
    echo $m . '<br>';
}

// 6
$nb = $pdo->query('SELECT COUNT(*) FROM materiel')->fetch();
echo "$nb matériels";
Voir la réponseMasquer la réponse de l'exercice 10
  1. Affectation au lieu d'une comparaison : = affecte 'gestionnaire' au rôle et la condition est toujours vraie. Pire, l'affectation est enregistrée dans la session : l'utilisateur reste gestionnaire jusqu'à sa déconnexion. Écrire === 'gestionnaire'.
  2. Tableau mal formé : [':id', $id] est une liste de deux valeurs, pas une association. Erreur SQLSTATE[HY093]: Invalid parameter number. Écrire ['id' => $id].
  3. exit manquant après header() : la redirection est envoyée, mais le script continue et supprime le matériel. Ajouter exit; (ou utiliser rediriger(), de type never). Une réponse 403 serait d'ailleurs plus juste qu'une redirection.
  4. password_verify() mal utilisé : il attend le mot de passe en clair et l'empreinte stockée. Hacher le mot de passe d'abord produit une nouvelle empreinte, avec un autre sel : la vérification échoue toujours. Écrire password_verify($motDePasse, $u['mot_de_passe']).
  5. fetch() dans un foreach : fetch() renvoie une ligne, et la boucle parcourt ses colonnes. Seule la première référence s'affiche. Écrire foreach ($stmt as $m) ou foreach ($stmt->fetchAll() as $m), puis echo e($m['reference']).
  6. fetch() renvoie un tableau (la ligne), pas la valeur : PHP affiche « Array matériels » avec l'avertissement « Array to string conversion ». Écrire $nb = (int) $pdo->query('SELECT COUNT(*) FROM materiel')->fetchColumn();.

Tu avais trouvé ?

Les réflexes à avoir le jour de l'examen

Tu vois…Tu penses…
Une variable de $_GET, $_POST ou $_COOKIE dans une chaîne SQLInjection SQL : requête préparée avec marqueur
prepare() sur une chaîne qui contient déjà des variablesAucune protection : marqueurs et execute()
<?= $variable ?> ou echo $variable sans htmlspecialchars()XSS : échapper à l'affichage
Un identifiant d'utilisateur lu dans l'URL ou un champ cachéIDOR : le lire en session, ou vérifier la propriété
Une suppression ou une modification déclenchée par un lien (GET)CSRF : formulaire POST avec jeton
md5(), sha1() ou hash() sur un mot de passeDéfaillance cryptographique : password_hash() et password_verify()
Deux messages différents « compte inconnu » et « mot de passe faux »Énumération de comptes : un message unique
header('Location: …') sans exitLe script continue après la redirection
Un mot de passe ou une clé écrit dans le codeSecret à sortir du code et à changer
$_FILES['…']['name'] ou ['type'] utilisé tel quelUpload dangereux : type réel, nom aléatoire, hors de la racine
display_errors = On en production, ou $e->getMessage() affichéFuite d'informations : journal et message neutre

Pour le reste de la préparation de l'écrit, le cours E7 SLAM propose des sujets et des épreuves blanches complètes ; pour l'oral, le cours E6 SLAM détaille les évolutions types et la recette.

Chapitre terminé !

Fais les 10 exercices avant de passer à la suite : c'est en t'entraînant que tout se fixe.

chapitres
–
de série
–
du cours
–
Cours suivant : POO en C#Revoir le sommaire du cours