Formation PUB010 : PHP, 2018 Sécuriser le code

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()).

▼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.

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. Retrouve les données de chacune des lignes de résultats.
            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);
}

Exemple avec requête SELECT

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

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();

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

            $stmt->fetch();
            echo "Bonjour, $prenom $nomfamille !";
        }
        else {
            echo "<div class='messageavertissement'>Le client demandé n'existe pas.</div>";
        } 
    } 
    else {
        echo "<div class='messageerreur'>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='messageerreur'>Nous sommes désolés, un problème technique nous empêche de retrouver les données (code 2).</div>";
    echo_debug($mysqli->error);
}

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

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();

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

            echo '<table>';

            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='messageavertissement'>Il n'y a aucun client né en $annee dans la ville $ville.</div>";
        } 
    } 
    else {
        echo "<div class='messageerreur'>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='messageerreur'>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 :

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.
$message = '';
 
if ('' == $prenom) {
    $message .= 'Le prénom est requis.<br />';
}
if ('' == $nomfamille) {
    $message .= 'Le nom de famille est requis.<br />';
}
 
if ('' == $message) {
 
    // 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='messageinformation'>Le client a été ajouté avec succès !</div>";
        }
        else {
            echo "<div class='messageerreur'>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='messageerreur'>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='messageerreur'>$message</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.

PHP

$prenom = '';
$nomfamille = '';
$_SESSION['reussite_ajout_client'] = false;
$_SESSION['message_ajout_client'] = "";
 
// Retrouve les données du formulaire.
if (isset($_POST['prenom']) {
    $prenom = $_POST['prenom'];
}
if (isset($_POST['nomfamille']) {
    $nomfamille = $_POST['nomfamille'];
}
 
// Valide les données.
$message = '';
 
if ('' == $prenom) {
    $message .= 'Le prénom est requis.<br />';
}
if ('' == $nomfamille) {
    $message .= 'Le nom de famille est requis.<br />';
}
 
if ('' == $message) {
 
    // 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['reussite_ajout_client'] = true;
            $_SESSION['message_ajout_client'] = "Le client a été ajouté avec succès !";
        }
        else {
            $_SESSION['message_ajout_client'] = "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_ajout_client'] = "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_ajout_client'] = $message;
}

Exemple avec DELETE

Voici finalement un exemple d'utilisation avec un DELETE :

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='messageinformation'>Le client a été supprimé avec succès !</div>";
        }
        else {
            echo "<div class='messageerreur'>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='messageerreur'>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='messageerreur'>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.
Dernière révision le 20 avril 2021
Merci de partager !

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

Soumettre