Qu’est-ce que l’injection SQL et comment la prévenir dans les applications PHP ?

Vous pensez donc que votre base de données SQL est performante et à l’abri d’une destruction instantanée ? Eh bien, l’injection SQL n’est pas d’accord !

Oui, c’est de la destruction instantanée dont nous parlons, car je ne veux pas ouvrir cet article avec la terminologie boiteuse habituelle de « renforcer la sécurité » et « empêcher les accès malveillants ». L’injection SQL est une astuce si ancienne dans le livre que tout le monde, chaque développeur, la connaît très bien et sait très bien comment l’empêcher. À l’exception de cette étrange fois où ils ont dérapé, et les résultats peuvent être tout simplement désastreux.

Si vous savez déjà ce qu’est l’injection SQL, n’hésitez pas à passer à la seconde moitié de l’article. Mais pour ceux qui débutent dans le domaine du développement Web et rêvent d’assumer des rôles plus importants, une introduction s’impose.

Qu’est-ce que l’Injection SQL ?

La clé pour comprendre SQL Injection est dans son nom : SQL + Injection. Le mot « injection » ici n’a aucune connotation médicale, mais plutôt l’utilisation du verbe « injecter ». Ensemble, ces deux mots véhiculent l’idée de mettre SQL dans une application Web.

Mettre SQL dans une application Web . . . hmmm . . . N’est-ce pas ce que nous faisons de toute façon ? Oui, mais nous ne voulons pas qu’un attaquant pilote notre base de données. Comprenons cela à l’aide d’un exemple.

Supposons que vous construisiez un site Web PHP typique pour un magasin de commerce électronique local, vous décidez donc d’ajouter un formulaire de contact comme celui-ci :

<form action="record_message.php" method="POST">
  <label>Your name</label>
  <input type="text" name="name">
  
  <label>Your message</label>
  <textarea name="message" rows="5"></textarea>
  
  <input type="submit" value="Send">
</form>

Et supposons que le fichier send_message.php stocke tout dans une base de données afin que les propriétaires de magasins puissent lire les messages des utilisateurs plus tard. Il peut avoir un code comme celui-ci :

<?php

$name = $_POST['name'];
$message = $_POST['message'];

// check if this user already has a message
mysqli_query($conn, "SELECT * from messages where name = $name");

// Other code here

Vous essayez donc d’abord de voir si cet utilisateur a déjà un message non lu. La requête SELECT * des messages où nom = $nom semble assez simple, n’est-ce pas ?

MAUVAIS!

Dans notre innocence, nous avons ouvert les portes à la destruction instantanée de notre base de données. Pour que cela se produise, l’attaquant doit remplir les conditions suivantes :

  • L’application s’exécute sur une base de données SQL (aujourd’hui, presque toutes les applications le sont)
  • La connexion actuelle à la base de données dispose des autorisations « modifier » et « supprimer » sur la base de données
  • Les noms des tables importantes peuvent être devinés

Le troisième point signifie que maintenant que l’attaquant sait que vous exploitez une boutique de commerce électronique, vous stockez très probablement les données de commande dans une table de commandes. Armé de tout cela, tout ce que l’attaquant doit faire est de fournir ceci comme nom :

Jo ; tronquer les commandes ; ? Oui Monsieur! Voyons ce que deviendra la requête lorsqu’elle sera exécutée par le script PHP :

SELECT * FROM messages WHERE nom = Joe; tronquer les commandes ;

D’accord, la première partie de la requête a une erreur de syntaxe (pas de guillemets autour de « Joe »), mais le point-virgule force le moteur MySQL à commencer à en interpréter une nouvelle : les commandes tronquées. Juste comme ça, d’un seul coup, tout l’historique des commandes a disparu !

Maintenant que vous savez comment fonctionne l’injection SQL, il est temps de voir comment l’arrêter. Les deux conditions qui doivent être remplies pour une injection SQL réussie sont :

  • Le script PHP doit avoir des privilèges de modification/suppression sur la base de données. Je pense que cela est vrai pour toutes les applications et que vous ne pourrez pas rendre vos applications en lecture seule. 🙂 Et devinez quoi, même si nous supprimons tous les privilèges de modification, l’injection SQL peut toujours permettre à quelqu’un d’exécuter des requêtes SELECT et d’afficher toute la base de données, données sensibles incluses. En d’autres termes, réduire le niveau d’accès à la base de données ne fonctionne pas et votre application en a besoin de toute façon.
  • L’entrée de l’utilisateur est en cours de traitement. La seule façon dont l’injection SQL peut fonctionner est lorsque vous acceptez les données des utilisateurs. Encore une fois, il n’est pas pratique d’arrêter toutes les entrées de votre application simplement parce que vous vous inquiétez de l’injection SQL.
  • Empêcher l’injection SQL dans PHP

    Maintenant, étant donné que les connexions à la base de données, les requêtes et les entrées utilisateur font partie de la vie, comment pouvons-nous empêcher l’injection SQL ? Heureusement, c’est assez simple et il y a deux façons de le faire : 1) nettoyer les entrées de l’utilisateur et 2) utiliser des instructions préparées.

    Assainir l’entrée de l’utilisateur

    Si vous utilisez une ancienne version de PHP (5.5 ou inférieure, et cela arrive souvent sur l’hébergement partagé), il est sage d’exécuter toutes vos entrées utilisateur via une fonction appelée mysql_real_escape_string(). Fondamentalement, ce qu’il fait, c’est supprimer tous les caractères spéciaux d’une chaîne afin qu’ils perdent leur signification lorsqu’ils sont utilisés par la base de données.

    Par exemple, si vous avez une chaîne comme je suis une chaîne, le caractère guillemet simple (‘) peut être utilisé par un attaquant pour manipuler la requête de base de données en cours de création et provoquer une injection SQL. L’exécuter via mysql_real_escape_string() produit Je suis une chaîne, qui ajoute une barre oblique inverse au guillemet simple, en l’échappant. Par conséquent, la chaîne entière est désormais transmise en tant que chaîne inoffensive à la base de données, au lieu de pouvoir participer à la manipulation des requêtes.

    Il y a un inconvénient avec cette approche : c’est une technique vraiment très ancienne qui va de pair avec les anciennes formes d’accès aux bases de données en PHP. Depuis PHP 7, cette fonction n’existe même plus, ce qui nous amène à notre solution suivante.

    Utiliser des déclarations préparées

    Les instructions préparées sont un moyen de rendre les requêtes de base de données plus sûres et plus fiables. L’idée est qu’au lieu d’envoyer la requête brute à la base de données, nous indiquons d’abord à la base de données la structure de la requête que nous allons envoyer. C’est ce que nous entendons par « préparer » une déclaration. Une fois qu’une instruction est préparée, nous transmettons les informations sous forme d’entrées paramétrées afin que la base de données puisse « combler les lacunes » en connectant les entrées à la structure de requête que nous avons envoyée auparavant. Cela enlève tout pouvoir spécial que les entrées pourraient avoir, les faisant être traitées comme de simples variables (ou charges utiles, si vous voulez) dans l’ensemble du processus. Voici à quoi ressemblent les instructions préparées :

    <?php
    $servername = "localhost";
    $username = "username";
    $password = "password";
    $dbname = "myDB";
    
    // Create connection
    $conn = new mysqli($servername, $username, $password, $dbname);
    
    // Check connection
    if ($conn->connect_error) {
        die("Connection failed: " . $conn->connect_error);
    }
    
    // prepare and bind
    $stmt = $conn->prepare("INSERT INTO MyGuests (firstname, lastname, email) VALUES (?, ?, ?)");
    $stmt->bind_param("sss", $firstname, $lastname, $email);
    
    // set parameters and execute
    $firstname = "John";
    $lastname = "Doe";
    $email = "[email protected]";
    $stmt->execute();
    
    $firstname = "Mary";
    $lastname = "Moe";
    $email = "[email protected]";
    $stmt->execute();
    
    $firstname = "Julie";
    $lastname = "Dooley";
    $email = "[email protected]";
    $stmt->execute();
    
    echo "New records created successfully";
    
    $stmt->close();
    $conn->close();
    ?>

    Je sais que le processus semble inutilement complexe si vous êtes nouveau dans les déclarations préparées, mais le concept en vaut la chandelle. Voici une belle introduction à celui-ci.

    Pour ceux qui connaissent déjà l’extension PDO de PHP et l’utilisent pour créer des instructions préparées, j’ai un petit conseil.

    Avertissement : soyez prudent lors de la configuration de PDO

    Lors de l’utilisation de PDO pour l’accès à la base de données, nous pouvons être aspirés par un faux sentiment de sécurité. « Ah, eh bien, j’utilise PDO. Maintenant, je n’ai plus besoin de penser à quoi que ce soit d’autre » — c’est ainsi que se déroule généralement notre façon de penser. Il est vrai que PDO (ou instructions préparées MySQLi) est suffisant pour empêcher toutes sortes d’attaques par injection SQL, mais vous devez être prudent lors de sa configuration. Il est courant de simplement copier-coller le code des didacticiels ou de vos projets précédents et de passer à autre chose, mais ce paramètre peut tout annuler :

    $dbConnection->setAttribute(PDO::ATTR_EMULATE_PREPARES, true);

    Ce que fait ce paramètre est de dire à PDO d’émuler des instructions préparées plutôt que d’utiliser réellement la fonction d’instructions préparées de la base de données. Par conséquent, PHP envoie des chaînes de requête simples à la base de données même si votre code semble créer des instructions préparées et définir des paramètres, etc. En d’autres termes, vous êtes aussi vulnérable à l’injection SQL qu’avant. 🙂

    La solution est simple : assurez-vous que cette émulation est définie sur false.

    $dbConnection->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);

    Désormais, le script PHP est obligé d’utiliser des instructions préparées au niveau de la base de données, empêchant toutes sortes d’injections SQL.

    Empêcher l’utilisation de WAF

    Savez-vous que vous pouvez également protéger les applications Web contre l’injection SQL en utilisant WAF (web application firewall) ?

    Eh bien, pas seulement l’injection SQL, mais bien d’autres vulnérabilités de la couche 7 telles que les scripts intersites, l’authentification cassée, la falsification intersites, l’exposition des données, etc. Vous pouvez soit utiliser l’auto-hébergement comme Mod Security, soit le cloud comme suit.

    Injection SQL et frameworks PHP modernes

    L’injection SQL est si courante, si facile, si frustrante et si dangereuse que tous les frameworks Web PHP modernes sont intégrés avec des contre-mesures. Dans WordPress, par exemple, nous avons la fonction $wpdb->prepare(), alors que si vous utilisez un framework MVC, il fait tout le sale boulot à votre place et vous n’avez même pas à penser à empêcher l’injection SQL. C’est un peu ennuyeux que dans WordPress, vous deviez préparer des déclarations explicitement, mais bon, c’est de WordPress dont nous parlons. 🙂

    Quoi qu’il en soit, ce que je veux dire, c’est que la race moderne de développeurs Web n’a pas à penser à l’injection SQL et, par conséquent, ils ne sont même pas conscients de cette possibilité. En tant que tel, même s’ils laissent une porte dérobée ouverte dans leur application (peut-être s’agit-il d’un paramètre de requête $_GET et de vieilles habitudes de déclenchement d’une requête sale), les résultats peuvent être catastrophiques. Il est donc toujours préférable de prendre le temps de plonger plus profondément dans les fondations.

    Conclusion

    L’injection SQL est une attaque très désagréable sur une application Web, mais elle est facilement évitée. Comme nous l’avons vu dans cet article, être prudent lors du traitement des entrées utilisateur (en passant, l’injection SQL n’est pas la seule menace que la gestion des entrées utilisateur apporte) et interroger la base de données est tout ce qu’il y a à faire. Cela dit, nous ne travaillons pas toujours dans la sécurité d’un framework web, il vaut donc mieux être conscient de ce type d’attaque et ne pas tomber dans le piège.