Aller au contenu
Réussir mon BTS SIO

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

Par exemple

Chapitre 929 minIntermédiaire

Inscription et connexion en PHP : password_hash et jeton CSRF

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

À la fin de ce chapitre, tu sauras

  • hacher et vérifier un mot de passe avec password_hash et password_verify ;
  • écrire une connexion qui ne révèle pas les comptes existants ;
  • protéger une page selon la connexion, le rôle et la propriété ;
  • ajouter un jeton anti-CSRF à chaque formulaire.

4 exercices corrigés

Un système de connexion en PHP vérifie l'identité d'un utilisateur : il compare le mot de passe saisi à l'empreinte stockée en base avec password_verify(), puis enregistre l'identifiant de l'utilisateur dans la session. Ce chapitre construit l'authentification complète de Prêtmat : création de compte avec password_hash(), connexion, limitation des tentatives, protection des pages selon le rôle, jeton anti-CSRF et déconnexion. Les notions générales (facteurs d'authentification, politique de mots de passe, MFA) sont dans le cours Authentification et habilitations.

Ce qu'il ne faut jamais faire avec un mot de passe

StockagePourquoi c'est une faute
En clairUne fuite de la base (injection, sauvegarde volée, administrateur indiscret) livre tous les mots de passe, que les utilisateurs réutilisent souvent ailleurs
Chiffré (réversible)Celui qui obtient la clé, souvent rangée à côté, déchiffre tout. Un mot de passe n'a jamais besoin d'être relu
Haché avec md5() ou sha1()Ces fonctions sont conçues pour être rapides : une carte graphique en calcule des milliards par seconde, et des tables précalculées donnent instantanément les mots de passe courants
Haché avec hash('sha256', …) sans selMême problème : rapide, et deux utilisateurs qui ont le même mot de passe ont la même empreinte

La bonne solution est une fonction de hachage lente et salée, conçue pour les mots de passe : bcrypt ou Argon2id. PHP les fournit avec password_hash(). Le cours Cryptographie explique le sel, le coût et le choix de l'algorithme.

Hacher un mot de passe avec password_hash

php
<?php

$empreinte = password_hash('Lampe-Bureau-Verte-42', PASSWORD_DEFAULT);
// $2y$12$fCf7kid8vDu8CufdWG4NBOfvzjBz0cXSTXAuxeGUxC.6MFg0FWdP6  (différente à chaque appel)

var_dump(password_verify('Lampe-Bureau-Verte-42', $empreinte));   // bool(true)
var_dump(password_verify('lampe-bureau-verte-42', $empreinte));   // bool(false)

L'empreinte contient tout ce qu'il faut pour vérifier : $2y$ désigne bcrypt, 12 le coût (2¹² tours de calcul, soit environ un quart de seconde), puis 22 caractères de sel aléatoire et 31 caractères d'empreinte. Comme le sel change à chaque appel, deux utilisateurs qui choisissent le même mot de passe ont des empreintes différentes, et l'on ne peut pas comparer deux empreintes entre elles : seule password_verify() sait vérifier.

ConstanteAlgorithmeRemarque
PASSWORD_DEFAULTbcrypt aujourd'huiLe choix recommandé : PHP le fera évoluer vers un algorithme plus fort sans que tu changes ton code
PASSWORD_BCRYPTbcryptCoût par défaut relevé de 10 à 12 en PHP 8.4. Seuls les 72 premiers octets du mot de passe comptent
PASSWORD_ARGON2IDArgon2idRecommandé par l'OWASP, disponible si PHP a été compilé avec ce support (c'est le cas des images Docker officielles)

Trois conséquences pratiques :

  • La colonne mot_de_passe est un VARCHAR(255) : l'empreinte bcrypt fait 60 caractères, celle d'Argon2id une centaine, et un futur algorithme pourra être plus long.
  • Avec bcrypt, deux mots de passe qui ont les mêmes 72 premiers octets sont équivalents. Limite la longueur saisie à 64 caractères, une limite généreuse qui reste sous ce seuil pour un texte sans accents, ou passe à Argon2id.
  • Un quart de seconde par connexion est imperceptible pour l'utilisateur, mais ralentit d'autant un attaquant qui essaie des millions de mots de passe sur une base volée. C'est le but.

Créer un compte

Dans Prêtmat, c'est le gestionnaire qui crée les comptes des employés. Une page d'inscription publique fonctionnerait de la même façon. La validation applique la politique de mots de passe de l'application : au moins 12 caractères, au plus 64, et différent de l'adresse email. Avec la limitation des tentatives de la section suivante, Prêtmat relève du deuxième cas de la CNIL (mot de passe associé à une restriction d'accès) ; pour un mot de passe utilisé seul, la CNIL demande 12 caractères des quatre types (majuscules, minuscules, chiffres, caractères spéciaux), 14 sans caractère spécial imposé, ou une phrase de passe de 7 mots (politique de mots de passe).

php
<?php

declare(strict_types=1);

const ROLES = ['employe', 'gestionnaire'];

function validerCompte(array $saisie): array
{
    $erreurs = [];

    $email = mb_strtolower(texte($saisie, 'email'));
    if (filter_var($email, FILTER_VALIDATE_EMAIL) === false || mb_strlen($email) > 180) {
        $erreurs['email'] = 'Adresse email invalide.';
    }

    $nom = texte($saisie, 'nom');
    if ($nom === '' || mb_strlen($nom) > 100) {
        $erreurs['nom'] = 'Le nom est obligatoire (100 caractères au plus).';
    }

    $role = texte($saisie, 'role');
    if (!in_array($role, ROLES, true)) {
        $erreurs['role'] = 'Rôle invalide.';
    }

    // Le mot de passe n'est pas « nettoyé » : un espace au début fait partie du mot de passe
    $motDePasse = is_string($saisie['mot_de_passe'] ?? null) ? $saisie['mot_de_passe'] : '';
    $longueur = mb_strlen($motDePasse);
    if ($longueur < 12 || $longueur > 64) {
        $erreurs['mot_de_passe'] = 'Le mot de passe doit contenir entre 12 et 64 caractères.';
    } elseif (mb_strtolower($motDePasse) === $email) {
        $erreurs['mot_de_passe'] = 'Le mot de passe ne doit pas être ton adresse email.';
    } elseif ($motDePasse !== ($saisie['confirmation'] ?? null)) {
        $erreurs['confirmation'] = 'Les deux mots de passe ne sont pas identiques.';
    }

    return [
        'valeurs' => ['email' => $email, 'nom' => $nom, 'role' => $role, 'mot_de_passe' => $motDePasse],
        'erreurs' => $erreurs,
    ];
}

function creerCompte(PDO $pdo, array $valeurs): int
{
    $stmt = $pdo->prepare(
        'INSERT INTO utilisateur (email, nom, mot_de_passe, role) VALUES (:email, :nom, :empreinte, :role)'
    );
    $stmt->execute([
        'email' => $valeurs['email'],
        'nom' => $valeurs['nom'],
        'empreinte' => password_hash($valeurs['mot_de_passe'], PASSWORD_DEFAULT),
        'role' => $valeurs['role'],
    ]);

    return (int) $pdo->lastInsertId();
}

Le traitement de la page appelle validerCompte(), puis creerCompte() dans un try qui transforme l'erreur 1062 (adresse déjà utilisée) en message sur le champ email (gérer une erreur SQL). Le mot de passe en clair n'est jamais enregistré, jamais écrit dans un journal, jamais renvoyé dans le formulaire : en cas d'erreur, le champ est réaffiché vide.

La connexion

php
<?php

declare(strict_types=1);

// Une empreinte quelconque, pour que la vérification prenne le même temps quand le compte n'existe pas
const EMPREINTE_FACTICE = '$2y$12$4Z0o/SE8qnngrBdBOzCQxevJLomoVXCaxA2aPtkdLnJOzmkS8DRw6';

/** Renvoie l'utilisateur si l'email et le mot de passe correspondent, null sinon. */
function authentifier(PDO $pdo, string $email, string $motDePasse): ?array
{
    $stmt = $pdo->prepare('SELECT id, nom, mot_de_passe, role FROM utilisateur WHERE email = :email');
    $stmt->execute(['email' => mb_strtolower(trim($email))]);
    $utilisateur = $stmt->fetch();

    $empreinte = $utilisateur === false ? EMPREINTE_FACTICE : $utilisateur['mot_de_passe'];
    if (!password_verify($motDePasse, $empreinte) || $utilisateur === false) {
        return null;
    }

    // Si l'algorithme ou le coût par défaut a changé, on recalcule l'empreinte maintenant
    if (password_needs_rehash($utilisateur['mot_de_passe'], PASSWORD_DEFAULT)) {
        $maj = $pdo->prepare('UPDATE utilisateur SET mot_de_passe = :empreinte WHERE id = :id');
        $maj->execute(['empreinte' => password_hash($motDePasse, PASSWORD_DEFAULT), 'id' => $utilisateur['id']]);
    }

    return $utilisateur;
}

Le traitement du formulaire de connexion :

php
<?php

declare(strict_types=1);

$erreur = null;

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    verifierCsrf();
    $email = texte($_POST, 'email');
    $motDePasse = is_string($_POST['mot_de_passe'] ?? null) ? $_POST['mot_de_passe'] : '';

    if (tropDeTentatives(bdd(), $email)) {
        $erreur = 'Trop de tentatives. Réessaie dans 15 minutes.';
    } else {
        $utilisateur = authentifier(bdd(), $email, $motDePasse);
        if ($utilisateur !== null) {
            session_regenerate_id(true);                       // contre la fixation de session
            $_SESSION['utilisateur'] = [
                'id' => $utilisateur['id'],
                'nom' => $utilisateur['nom'],
                'role' => $utilisateur['role'],
            ];
            $_SESSION['derniere_activite'] = time();
            header('Location: /materiels.php', true, 303);
            exit;
        }
        noterEchec(bdd(), $email);
        $erreur = 'Email ou mot de passe incorrect.';
    }
}

Chaque détail a une raison :

  • Un seul message d'erreur, « Email ou mot de passe incorrect », que l'adresse existe ou non. Deux messages différents (« Compte inconnu », « Mot de passe faux ») permettraient de dresser la liste des adresses inscrites : c'est l'énumération de comptes.
  • L'empreinte factice : sans elle, la réponse serait plus rapide pour une adresse inconnue (pas de calcul bcrypt), ce qui trahirait aussi l'existence des comptes.
  • session_regenerate_id(true) juste après la réussite : l'identifiant de session d'avant la connexion ne vaut plus rien (fixation de session).
  • La session ne contient pas l'empreinte : seulement l'identifiant, le nom et le rôle.
  • password_needs_rehash() fait migrer les empreintes en douceur : quand PHP relève le coût par défaut ou change d'algorithme, chaque compte est mis à jour à sa prochaine connexion, au seul moment où l'on connaît le mot de passe en clair.

Limiter les tentatives

Sans limite, un programme peut essayer des milliers de mots de passe par minute sur un compte : c'est une attaque par force brute, ou par dictionnaire quand il essaie les mots de passe les plus courants. Prêtmat bloque un compte pendant 15 minutes après 5 échecs. Les échecs sont notés dans une table :

sql
CREATE TABLE echec_connexion (
  id INT AUTO_INCREMENT PRIMARY KEY,
  email VARCHAR(180) NOT NULL,
  ip VARCHAR(45) NOT NULL,
  le DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  INDEX idx_echec_email_le (email, le)
);
php
<?php

declare(strict_types=1);

const ECHECS_MAX = 5;

function tropDeTentatives(PDO $pdo, string $email): bool
{
    $stmt = $pdo->prepare(
        'SELECT COUNT(*) FROM echec_connexion
         WHERE email = :email AND le > NOW() - INTERVAL 15 MINUTE'
    );
    $stmt->execute(['email' => mb_strtolower(trim($email))]);
    return (int) $stmt->fetchColumn() >= ECHECS_MAX;
}

function noterEchec(PDO $pdo, string $email): void
{
    $stmt = $pdo->prepare('INSERT INTO echec_connexion (email, ip) VALUES (:email, :ip)');
    $stmt->execute(['email' => mb_strtolower(trim($email)), 'ip' => $_SERVER['REMOTE_ADDR'] ?? '']);
}

La colonne ip sert à repérer une attaque qui essaie un mot de passe sur beaucoup de comptes ; c'est une donnée personnelle, à purger régulièrement (une tâche planifiée qui supprime les lignes de plus de 30 jours). Ces échecs sont aussi des événements de sécurité à journaliser (journalisation). Une application exposée sur Internet ajoute souvent une double authentification (MFA) pour les comptes à privilèges.

Protéger une page et vérifier le rôle

Cacher le lien « Ajouter un matériel » aux employés ne protège rien : un employé qui connaît l'adresse /ajouter.php peut l'ouvrir. Chaque page, chaque action vérifie elle-même que l'utilisateur est connecté et qu'il a le bon rôle. Trois fonctions suffisent :

php
<?php

declare(strict_types=1);

// src/auth.php
function utilisateurConnecte(): ?array
{
    return $_SESSION['utilisateur'] ?? null;
}

function exigerConnexion(): array
{
    $utilisateur = utilisateurConnecte();
    if ($utilisateur === null) {
        flash('info', 'Connecte-toi pour accéder à cette page.');
        header('Location: /connexion.php', true, 303);
        exit;
    }
    return $utilisateur;
}

function exigerRole(string $role): array
{
    $utilisateur = exigerConnexion();
    if ($utilisateur['role'] !== $role) {
        http_response_code(403);
        exit('Accès refusé.');
    }
    return $utilisateur;
}
php
<?php
// En tête de ajouter.php : seul le gestionnaire peut ajouter du matériel
$utilisateur = exigerRole('gestionnaire');

Le rôle ne suffit pas toujours. Un employé peut consulter ses emprunts, pas ceux des autres. Si la page reçoit l'identifiant de l'emprunt dans l'URL (emprunt.php?id=12), il faut vérifier que cet emprunt lui appartient, sinon il lui suffit de changer le nombre pour lire les emprunts de ses collègues. Cette faille s'appelle une IDOR (Insecure Direct Object Reference), la forme la plus courante du contrôle d'accès défaillant, premier risque du Top 10 de l'OWASP :

php
<?php

$utilisateur = exigerConnexion();

$stmt = bdd()->prepare('SELECT * FROM emprunt WHERE id = :id');
$stmt->execute(['id' => $idEmprunt]);
$emprunt = $stmt->fetch();

$estLeSien = $emprunt !== false && $emprunt['id_utilisateur'] === $utilisateur['id'];
if (!$estLeSien && $utilisateur['role'] !== 'gestionnaire') {
    http_response_code(404);   // 404 plutôt que 403 : on ne confirme pas que l'emprunt existe
    exit('Emprunt introuvable.');
}

Le cours Sécurité des applications détaille le contrôle d'accès et ses variantes.

Le jeton anti-CSRF

Le navigateur joint automatiquement le cookie de session à toute requête vers Prêtmat, y compris quand elle part d'une page d'un autre site. Une page piégée peut donc contenir un formulaire caché qui s'envoie tout seul vers https://pretmat.sorbier.example/supprimer.php : si le gestionnaire est connecté et visite cette page, la suppression s'exécute en son nom, à son insu. C'est la falsification de requête intersite (Cross-Site Request Forgery, CSRF).

La parade : un jeton aléatoire, stocké en session et ajouté en champ caché à chaque formulaire. Le serveur refuse tout POST qui n'a pas le bon jeton. La page piégée, hébergée sur un autre site, ne peut pas lire les pages de Prêtmat : elle ne connaît pas le jeton.

php
<?php

declare(strict_types=1);

// src/csrf.php
function jetonCsrf(): string
{
    return $_SESSION['csrf'] ??= bin2hex(random_bytes(32));
}

function champCsrf(): string
{
    return '<input type="hidden" name="csrf" value="' . jetonCsrf() . '">';
}

function verifierCsrf(): void
{
    $recu = $_POST['csrf'] ?? '';
    if (!is_string($recu) || !hash_equals(jetonCsrf(), $recu)) {
        http_response_code(403);
        exit('Requête refusée : le formulaire a expiré. Recharge la page et recommence.');
    }
}
php
<form method="post" action="/supprimer.php">
  <?= champCsrf() ?>
  <input type="hidden" name="id" value="<?= e($m['id']) ?>">
  <button>Supprimer</button>
</form>
  • random_bytes(32) donne 32 octets imprévisibles (64 caractères hexadécimaux) : on ne devine pas le jeton.
  • hash_equals() compare en temps constant : une comparaison ordinaire s'arrête au premier caractère différent, et la durée de la réponse pourrait aider à deviner le jeton.
  • verifierCsrf() s'appelle au début du traitement de chaque requête POST, avant toute action. Dans une application organisée, le contrôleur frontal le fait pour toutes les requêtes POST d'un coup (chapitre 12).
  • L'attribut SameSite=Lax du cookie de session bloque déjà la plupart de ces requêtes ; le jeton reste la protection de référence, qui ne dépend pas du navigateur.

Le mécanisme et ses variantes sont détaillés dans le cours Sécurité des applications.

La déconnexion

La déconnexion modifie l'état de la session : c'est une action en POST, avec le jeton.

php
<form method="post" action="/deconnexion.php">
  <?= champCsrf() ?>
  <button>Se déconnecter</button>
</form>
php
<?php
// deconnexion.php
verifierCsrf();
deconnecter();                 // chapitre 6 : vide la session, supprime le cookie et le fichier
flash('info', 'Tu es déconnecté.');
header('Location: /connexion.php', true, 303);
exit;

Mot de passe oublié : le principe

Une fonction « mot de passe oublié » ne renvoie jamais l'ancien mot de passe (on ne le connaît pas) et n'en envoie pas un nouveau en clair par mail. Le principe :

  1. L'utilisateur saisit son adresse. La page répond toujours « Si un compte existe, un lien t'a été envoyé », pour ne pas révéler les comptes.
  2. Si le compte existe, le serveur génère un jeton aléatoire (bin2hex(random_bytes(32))), en enregistre l'empreinte (hash('sha256', $jeton)) en base avec une date d'expiration courte (30 minutes), et envoie par mail un lien qui contient le jeton.
  3. Au clic, le serveur retrouve l'empreinte, vérifie l'expiration, laisse choisir un nouveau mot de passe, puis supprime le jeton : il ne sert qu'une fois.

Un SHA-256 suffit ici, contrairement aux mots de passe : le jeton est long et aléatoire, il ne se devine pas par dictionnaire.

Exercices

Exercice 1 sur 4 Corriger une connexion, comme à l'E7 Réussi À revoir

Relève les défauts de ce script de connexion, classe-les par gravité et réécris-le.

php
<?php
session_start();
$email = $_POST['email'];
$mdp = md5($_POST['mdp']);
$sql = "SELECT * FROM utilisateur WHERE email = '$email'";
$u = $pdo->query($sql)->fetch();
if (!$u) {
    echo "Aucun compte avec cette adresse";
} elseif ($u['mot_de_passe'] == $mdp) {
    $_SESSION['utilisateur'] = $u;
    header('Location: accueil.php');
} else {
    echo "Mot de passe incorrect";
}
Voir la réponseMasquer la réponse de l'exercice 1

Du plus grave au moins grave :

  1. Injection SQL : $email est concaténé dans la requête. Requête préparée avec un marqueur.
  2. Hachage faible : md5() est rapide et sans sel ; une base volée se casse en quelques minutes pour les mots de passe courants. password_hash() à l'enregistrement, password_verify() ici. Les comptes existants migrent à leur prochaine connexion.
  3. Comparaison souple == entre deux empreintes : deux chaînes de chiffres hexadécimaux qui ressemblent à des nombres peuvent être jugées égales. password_verify() règle aussi ce point.
  4. Énumération de comptes : deux messages différents révèlent les adresses inscrites. Un message unique.
  5. Fixation de session : pas de session_regenerate_id(true) après la connexion.
  6. Toute la ligne en session, empreinte comprise : on ne garde que l'identifiant, le nom et le rôle.
  7. Ni jeton CSRF, ni limitation des tentatives, ni exit après header(), et $_POST['email'] lu sans vérifier qu'il existe.
php
<?php
declare(strict_types=1);
session_start();

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    verifierCsrf();
    $email = texte($_POST, 'email');
    $mdp = is_string($_POST['mdp'] ?? null) ? $_POST['mdp'] : '';

    if (tropDeTentatives($pdo, $email)) {
        $erreur = 'Trop de tentatives. Réessaie dans 15 minutes.';
    } elseif (($u = authentifier($pdo, $email, $mdp)) !== null) {
        session_regenerate_id(true);
        $_SESSION['utilisateur'] = ['id' => $u['id'], 'nom' => $u['nom'], 'role' => $u['role']];
        header('Location: /accueil.php', true, 303);
        exit;
    } else {
        noterEchec($pdo, $email);
        $erreur = 'Email ou mot de passe incorrect.';
    }
}

authentifier() est la fonction de la section La connexion : requête préparée, password_verify() et empreinte factice.

Tu avais trouvé ?

Exercice 2 sur 4 Migrer des empreintes SHA-1 Réussi À revoir

L'ancienne version de Prêtmat stockait sha1($motDePasse) (40 caractères hexadécimaux). Sans demander à tous les utilisateurs de changer de mot de passe, comment passer à password_hash() ? Écris la partie de authentifier() qui gère les deux formats.

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

On reconnaît l'ancien format à sa forme (40 caractères hexadécimaux, alors qu'une empreinte password_hash() commence par $). À la connexion, si l'ancienne empreinte correspond, on connaît le mot de passe en clair : on calcule la nouvelle empreinte et on la stocke.

php
<?php
$empreinte = $utilisateur['mot_de_passe'];
$ancienFormat = preg_match('/^[0-9a-f]{40}$/', $empreinte) === 1;

$valide = $ancienFormat
    ? hash_equals($empreinte, sha1($motDePasse))
    : password_verify($motDePasse, $empreinte);

if ($valide && ($ancienFormat || password_needs_rehash($empreinte, PASSWORD_DEFAULT))) {
    $maj = $pdo->prepare('UPDATE utilisateur SET mot_de_passe = :e WHERE id = :id');
    $maj->execute(['e' => password_hash($motDePasse, PASSWORD_DEFAULT), 'id' => $utilisateur['id']]);
}

Les comptes qui ne se reconnectent jamais gardent leur SHA-1 : au bout d'un délai, on les force à réinitialiser leur mot de passe par mail. Une autre méthode traite tout le monde d'un coup : enrober l'ancienne empreinte (password_hash($sha1Existant, PASSWORD_DEFAULT)) et vérifier avec password_verify(sha1($motDePasse), $empreinte) pour ces comptes marqués.

Tu avais trouvé ?

Exercice 3 sur 4 Une IDOR dans les emprunts Réussi À revoir

La page mes-emprunts.php?utilisateur=3 affiche les emprunts de l'utilisateur dont l'identifiant est dans l'URL. Le lien du menu est construit avec l'identifiant de la personne connectée. Julie (id 2) est connectée. Que se passe-t-il si elle change l'URL en mes-emprunts.php?utilisateur=3 ? Corrige la page.

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

Julie voit les emprunts de Karim Morel (id 3) : rien ne vérifie que l'identifiant de l'URL est le sien. C'est une IDOR, un contrôle d'accès défaillant. Elle pourrait essayer tous les identifiants et lire les emprunts de toute l'entreprise.

Une page « mes emprunts » n'a pas besoin de paramètre : l'identifiant se lit dans la session, que l'utilisateur ne peut pas modifier.

php
<?php
$utilisateur = exigerConnexion();

$stmt = bdd()->prepare(
    'SELECT m.reference, m.libelle, e.date_debut, e.date_retour_prevue
     FROM emprunt e JOIN materiel m ON m.id = e.id_materiel
     WHERE e.id_utilisateur = :id AND e.date_retour IS NULL
     ORDER BY e.date_retour_prevue'
);
$stmt->execute(['id' => $utilisateur['id']]);
$emprunts = $stmt->fetchAll();

Si le gestionnaire doit pouvoir consulter les emprunts de n'importe qui, c'est une autre page (emprunts-utilisateur.php?id=3), protégée par exigerRole('gestionnaire').

Tu avais trouvé ?

Exercice 4 sur 4 Ajouter la protection CSRF Réussi À revoir

Le gestionnaire valide les retours avec ce formulaire, traité par retour.php. Ajoute la protection anti-CSRF des deux côtés. Pourquoi faut-il aussi vérifier le rôle dans retour.php ?

php
<form method="post" action="/retour.php">
  <input type="hidden" name="id_emprunt" value="<?= e($emprunt['id']) ?>">
  <label><input type="checkbox" name="en_panne" value="1"> Rendu en panne</label>
  <button>Enregistrer le retour</button>
</form>
Voir la réponseMasquer la réponse de l'exercice 4

Dans le formulaire, une ligne :

php
<form method="post" action="/retour.php">
  <?= champCsrf() ?>
  <input type="hidden" name="id_emprunt" value="<?= e($emprunt['id']) ?>">
  <label><input type="checkbox" name="en_panne" value="1"> Rendu en panne</label>
  <button>Enregistrer le retour</button>
</form>

Au début de retour.php, avant toute action :

php
<?php
declare(strict_types=1);

exigerRole('gestionnaire');
verifierCsrf();

$idEmprunt = filter_input(INPUT_POST, 'id_emprunt', FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]]);
$enPanne = isset($_POST['en_panne']);   // une case non cochée n'est pas envoyée

Le jeton prouve que la requête vient d'un formulaire de Prêtmat ; il ne dit rien de qui l'envoie. Un employé connecté a lui aussi un jeton valide dans sa session : sans exigerRole(), il pourrait envoyer cette requête lui-même et clore n'importe quel emprunt. Les deux contrôles sont nécessaires.

Tu avais trouvé ?

Chapitre terminé !

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

chapitres
–
de série
–
du cours
–
Chapitre suivant : CRUD complet en PHP avec PDO et MySQLRevoir le sommaire du cours