Formation PUB010 : PHP, 2022 Sécuriser le code

21.6 Les requêtes préparées


***** CETTE PAGE EST IMPORTANTE. PLACEZ-LA DANS VOS FAVORIS *****

Lorsqu'une requête SQL doit utiliser une valeur tirée d'une variable, nous exposons notre base de données aux injections SQL. Il s'agit d'une technique utilisée par les utilisateurs malveillants pour tenter d'obtenir des informations sensibles tirées de la base de données.

C'est pourquoi, lorsqu'une requête contient une valeur tirée d'une variable, nous utiliserons toujours des requêtes préparées, aussi appelées requêtes paramétrables, afin de protéger nos données contre les injections SQL.

Notez que si votre requête ne contient aucune valeur tirée d'une variable PHP, il est préférable d'utiliser une requête régulière (structure de code avec $mysqli->query()).

Dans cette fiche, je commence par vous expliquer en détails comment une requête préparée doit être réalisée pour que votre code soit robuste, convivial, performant et fonctionnel. 

Je vous donne ensuite des exemples d'utilisation :

▼Publicité Le texte se poursuit plus bas

Structure du code

Votre programme devra utiliser la structure de code suivante. De nombreux commentaires ont été ajoutés pour vous aider à bien comprendre chacune des étapes.

Selon la documentation officielle de PHP, l'exécution d'une requête préparée se déroule en deux étapes : la préparation et l'exécution.

Lors de la préparation, un template de requête est envoyé au serveur de base de données. Le serveur effectue une vérification de la syntaxe, et initialise les ressources internes du serveur pour une utilisation ultérieure.

La préparation est suivie de l'exécution. Pendant l'exécution, le client lie les valeurs des paramètres et les envoie au serveur. Le serveur crée une requête depuis le template et y lie les valeurs pour l'exécution, en utilisant les ressources internes créées précédemment.

La syntaxe sera légèrement difféfente selon la version de PHP utilisée.

PHP < 8

Syntaxe PHP

// 1. Dans la requête il faut placer un ? à la place de chaque paramètre (chaque variable utilisée dans la requête).
//    Remarquez qu'avec les requêtes préparées, PHP s'occupera d'ajouter lui-même les apostrophes de chaque côté d'une variable string.
$requete = "SELECT champ1, champ2, champ3 FROM table1 WHERE champ4=? OR champ5=?";


// 2. Prépare la requête (MySQL connaîtra le but de la requête avant même de connaître la valeur des variables). 
//    Il est d'usage d'utiliser une variable nommée stmt (StaTeMenT).
$stmt = $mysqli->prepare($requete);
 
if ($stmt) {
 
    // 3. Indique le type de chacun des paramètres : string (s), integer (i) ou decimal (d).
    //    Assigne ensuite à chacun des paramètres, dans l'ordre, la variable qui contient sa valeur. 
    $stmt->bind_param('xx', $var1, $var2);
 
    // 4. Exécute la requête. 
    $stmt->execute();
    // Sans cette ligne, il ne sera pas possible de connaître le nombre de lignes retournées par un SELECT.
    $stmt->store_result(); 
 
    // Si la requête a fonctionné
    if (0 == $stmt->errno) {
        if ($stmt->num_rows > 0) {
            // Pour une requête INSERT, UPDATE ou DELETE, travailler plutôt avec $stmt->affected_rows
 
            // 5. Fait le lien entre la position des champs lus par le SELECT et les variables qui seront initialisées lors du fetch.
            //    Cette étape n'aura pas lieu si la requête était un INSERT, un UPDATE ou un DELETE.
            $stmt->bind_result($champ1, $champ2, $champ3);
 
            // 6. Pour chaque enregistrement, initialise les variables du bind_result() à partir des champs lus.
            while ($stmt->fetch()) {
                ...
            }
        }
        else {
            // aucun enregistrement ne correspond à la requête ou aucun enregistrement n'a été modifié par la requête.
            ...
        }
    }
    else { 
        // Arrivera ici si les paramètres posent problème (ex : contrainte d'intégrité référentielle non respectée ou erreur dans le bind_param()).
        ...
        echo_debug($stmt->error);
    }
  
    // 7. Libère la mémoire (doit être fait à la fin du if ($stmt)).
    $stmt->close();
}
else {
    // Arrivera ici s'il y a une erreur dans la requête (ex : mauvais nom de champ).
    ...
    echo_debug($mysqli->error);
}

PHP 8 et plus

Syntaxe PHP

// 1. Dans la requête il faut placer un ? à la place de chaque paramètre (chaque variable utilisée dans la requête).
//    Remarquez qu'avec les requêtes préparées, PHP s'occupera d'ajouter lui-même les apostrophes de chaque côté d'une variable string.
$requete = "SELECT champ1, champ2, champ3 FROM table1 WHERE champ4=? OR champ5=?";


// 2. Prépare la requête (MySQL connaîtra le but de la requête avant même de connaître la valeur des variables). 
//    Il est d'usage d'utiliser une variable nommée stmt (StaTeMenT).
try {
    $stmt = $mysqli->prepare($requete);
 
    // 3. Indique le type de chacun des paramètres : string (s), integer (i) ou decimal (d).
    //    Assigne ensuite à chacun des paramètres, dans l'ordre, la variable qui contient sa valeur. 
    $stmt->bind_param('xx', $var1, $var2);
 
    // 4. Exécute la requête. 
    $stmt->execute();
    // Sans cette ligne, il ne sera pas possible de connaître le nombre de lignes retournées par un SELECT.
    $stmt->store_result(); 
 
    // Si la requête a fonctionné
    if (0 == $stmt->errno) {
        if ($stmt->num_rows > 0) {
            // Pour une requête INSERT, UPDATE ou DELETE, travailler plutôt avec $stmt->affected_rows
 
            // 5. Fait le lien entre la position des champs lus par le SELECT et les variables qui seront initialisées lors du fetch.
            //    Cette étape n'aura pas lieu si la requête était un INSERT, un UPDATE ou un DELETE.
            $stmt->bind_result($champ1, $champ2, $champ3);
 
            // 6. Pour chaque enregistrement, initialise les variables du bind_result() à partir des champs lus.
            while ($stmt->fetch()) {
                ...
            }
        }
        else {
            // aucun enregistrement ne correspond à la requête ou aucun enregistrement n'a été modifié par la requête.
            ...
        }
    }
    else { 
        // Arrivera ici si les paramètres posent problème (ex : contrainte d'intégrité référentielle non respectée ou erreur dans le bind_param()).
        ...
        echo_debug($stmt->error);
    }
  
    // 7. Libère la mémoire (doit être fait à la fin du if ($stmt)).
    $stmt->close();
} catch (Exception $e) {
    // Arrivera ici s'il y a une erreur dans la requête (ex : mauvais nom de champ).
    ...
    echo_debug($mysqli->error);
}

Exemple avec requête SELECT

Voici un exemple pour une requête SELECT qui ne retourne jamais plus d'un enregistrement.

Notez que ce code est adapté pour PHP 7.x. Si vous travaillez avec PHP 8.x, vous devrez y apporter quelques ajustements.

PHP

$id = $_POST['id'];

$requete = "SELECT prenom, nomfamille FROM clients WHERE id=?";
$stmt = $mysqli->prepare($requete);

if ($stmt) {

    $stmt->bind_param('i', $id);

    $stmt->execute();
    $stmt->store_result();   // nécessaire pour pouvoir travailler avec $stmt->num_rows

    if (0 == $stmt->errno) {
        if ($stmt->num_rows > 0) {
            $stmt->bind_result($prenom, $nomfamille);

            $stmt->fetch();   // c'est ici que $prenom, $nomfamille sont initialisés
            echo "Bonjour, $prenom $nomfamille !";
        }
        else {
            echo "<div class='message-avertissement'>Le client demandé n'existe pas.</div>";
        } 
    } 
    else {
        echo "<div class='message-erreur'>Nous sommes désolés, un problème technique nous empêche de retrouver les données (code 1).</div>";
        echo_debug($stmt->error);
    }

    $stmt->close();
}
else {
    echo "<div class='message-erreur'>Nous sommes désolés, un problème technique nous empêche de retrouver les données (code 2).</div>";
    echo_debug($mysqli->error);
}

Exemple avec requête SELECT et while

Dans ce second exemple, nous travaillons avec une requête qui peut retourner plusieurs enregistrements.

Notez que ce code est adapté pour PHP 7.x. Si vous travaillez avec PHP 8.x, vous devrez y apporter quelques ajustements.

PHP

$ville = '';
$annee = -1;
 
if (isset($_GET['ville']) {
    $ville = strtoupper($_GET['ville']);   // dans cet exemple, on convertit en majuscules puisqu'il y a un UPPER dans la requête
}
if (isset($_GET['annee']) {
    $annee = $_GET['annee'];
}

$requete = "SELECT id, prenom, nomfamille FROM clients WHERE UPPER(ville)=? AND EXTRACT(year FROM naissance) = ? ORDER BY nomfamille, prenom";
$stmt = $mysqli->prepare($requete);

if ($stmt) {

    $stmt->bind_param('si', $ville, $annee);

    $stmt->execute();
    $stmt->store_result();   // nécessaire pour pouvoir travailler avec $stmt->num_rows

    if (0 == $stmt->errno) {
        if ($stmt->num_rows > 0) {
            $stmt->bind_result($id, $prenom, $nomfamille);

            echo '<table>';

            // Pour chaque enregistrement, initialise les variables du bind_result() à partir des champs lus.
            while ($stmt->fetch()) {
                echo "<tr><td><a href='detailsclient.php?id=$id'>Détails</a></td><td>$nomfamille</td><td>$prenom</td></tr>";
            }

            echo '</table>';

        }
        else {
            echo "<div class='message-avertissement'>Il n'y a aucun client né en $annee dans la ville $ville.</div>";
        } 
    } 
    else {
        echo "<div class='message-erreur'>Nous sommes désolés, un problème technique nous empêche de retrouver les données (code 1).</div>";
        echo_debug($stmt->error);
    }

    $stmt->close();
}
else {
    echo "<div class='message-erreur'>Nous sommes désolés, un problème technique nous empêche de retrouver les données (code 2).</div>";
    echo_debug($mysqli->error);
}

Exemple avec INSERT

Cet autre exemple illustre l'utilisation d'une requête préparée lors d'un INSERT.

Attention : si votre code doit faire une redirection, vous devrez plutôt utiliser la version présentée plus bas, qui initialise des variables de session.

Notez que ce code est adapté pour PHP 7.x. Si vous travaillez avec PHP 8.x, vous devrez y apporter quelques ajustements.

PHP

$prenom = '';
$nomfamille = '';
 
// Retrouve les données du formulaire.
if (isset($_POST['prenom']) {
    $prenom = $_POST['prenom'];
}
if (isset($_POST['nomfamille']) {
    $nomfamille = $_POST['nomfamille'];
}
 
// Valide les données.
$messageErreur = '';
 
// refaire en PHP toutes les validations HTML et JavaScript
 
// Si tout était valide
if ('' == $messageErreur) {
 
    // Tente l'enregistrement des données.
    $requete = "INSERT INTO clients(prenom, nomfamille, actif) VALUES(?, ?, 1)";
    $stmt = $mysqli->prepare($requete);
 
    if ($stmt) {
 
        $stmt->bind_param('ss', $prenom, $nomfamille);
 
        $stmt->execute();
 
        if (0 == $stmt->errno) {
            echo "<div class='message-information'>Le client a été ajouté avec succès !</div>";
        }
        else {
            echo "<div class='message-erreur'>Nous sommes désolés, un problème technique nous empêche d'enregistrer le client (code 1).</div>";
            echo_debug($stmt->error);
        }
 
        $stmt->close();
    }
    else {
        echo "<div class='message-erreur'>Nous sommes désolés, un problème technique nous empêche d'enregistrer le client (code 2).</div>";
        echo_debug($mysqli->error);
    }
}
else {
    echo "<div class='message-erreur'>$messageErreur</div>";
}

Exemple avec INSERT et variables de session

Parfois, la page qui réalise le INSERT doit effectuer une redirection après avoir accompli son travail. Elle n'affichera donc rien à l'écran.

Voici à nouveau l'extrait de code précédent mais cette fois, il travaille avec des variables de session.

Notez que ce code est adapté pour PHP 7.x. Si vous travaillez avec PHP 8.x, vous devrez y apporter quelques ajustements.

PHP

$prenom = '';
$nomfamille = '';
$_SESSION['operation_reussie'] = false;
$_SESSION['message_operation'] = "";
 
// Retrouve les données du formulaire.
if (isset($_POST['prenom']) {
    $prenom = $_POST['prenom'];
}
if (isset($_POST['nomfamille']) {
    $nomfamille = $_POST['nomfamille'];
}
 
// Valide les données.
$messageErreur = '';
 
// refaire en PHP toutes les validations HTML et JavaScript
 
// Si tout était valide
if ('' == $messageErreur) {
 
    // Tente l'enregistrement des données.
    $requete = "INSERT INTO clients(prenom, nomfamille, actif) VALUES(?, ?, 1)";
    $stmt = $mysqli->prepare($requete);
 
    if ($stmt) {
 
        $stmt->bind_param('ss', $prenom, $nomfamille);
 
        $stmt->execute();
 
        if (0 == $stmt->errno) {
            $_SESSION['operation_reussie'] = true;
            $_SESSION['message_operation'] = "Le client a été ajouté avec succès !";
        }
        else {
            $_SESSION['message_operation'] = "Nous sommes désolés, un problème technique nous empêche d'enregistrer le client (code 1).";
            log_debug($stmt->error);
        }
 
        $stmt->close();
    }
    else {
        $_SESSION['message_operation'] = "Nous sommes désolés, un problème technique nous empêche d'enregistrer le client (code 2).";
        log_debug($mysqli->error);
    }
}
else {
    $_SESSION['message_operation'] = $messageErreur;
}

Exemple avec DELETE

Voici finalement un exemple d'utilisation avec un DELETE.

Notez que ce code est adapté pour PHP 7.x. Si vous travaillez avec PHP 8.x, vous devrez y apporter quelques ajustements.

PHP

// Retrouve l'information dans l'URL.
if (isset($_GET['id']) {
    $id = $_GET['id'];
}
else {
    $id = -1;
}
   
// Tente la suppression des données.
$requete = "DELETE FROM clients WHERE id=?";
$stmt = $mysqli->prepare($requete);
 
if ($stmt) {
   
    $stmt->bind_param('i', $id);
 
    $stmt->execute();
 
    if (0 == $stmt->errno) {
        // si l'id n'existe pas, ça ne génère pas d'erreur mais ça ne supprime rien.
        if ($stmt->affected_rows > 0) {
            echo "<div class='message-information'>Le client a été supprimé avec succès !</div>";
        }
        else {
            echo "<div class='message-erreur'>Nous sommes désolés, un problème technique nous empêche de supprimer le client (code 1).</div>";
            echo_debug("Id de client non trouvé pour la suppression : $id");
        }
    }
    else {
        echo "<div class='message-erreur'>Nous sommes désolés, un problème technique nous empêche de supprimer le client (code 2).</div>";
        echo_debug($stmt->error);
    }
 
    $stmt->close();
}
else {
    echo "<div class='message-erreur'>Nous sommes désolés, un problème technique nous empêche de supprimer le client (code 3).</div>";
    echo_debug($mysqli->error);

Pour plus d'information

« Les requêtes préparées ». PHP. http://php.net/manual/fr/mysqli.quickstart.prepared-statements.php

Veuillez noter que le contenu de cette fiche vous est partagé à titre gracieux, au meilleur de mes connaissances et sans aucune garantie.
Par Christiane Lagacé
Dernière révision le 29 février 2024
Merci de partager !

Site fièrement hébergé chez A2 Hosting.

Soumettre