Vous croyez que votre base de données SQL fonctionne parfaitement et est à l’abri de toute destruction subite ? Détrompez-vous, l’injection SQL peut vous prouver le contraire !
Oui, nous parlons bien de destruction immédiate, car je ne souhaite pas débuter cet article avec les sempiternelles expressions telles que « renforcer la sécurité » ou « empêcher les accès malveillants ». L’injection SQL est une technique si ancienne que chaque développeur la connaît parfaitement et sait comment s’en prémunir. Excepté, bien sûr, cette fois étrange où l’on oublie toutes ces précautions, et les conséquences peuvent être absolument désastreuses.
Si vous êtes déjà familier avec l’injection SQL, n’hésitez pas à passer à la seconde partie de cet article. Mais pour ceux qui débutent dans le développement web et aspirent à prendre des responsabilités plus importantes, une introduction est nécessaire.
Qu’est-ce que l’injection SQL ?
La clé pour comprendre l’injection SQL réside dans son nom : SQL + Injection. Le mot « injection » ici n’a aucune connotation médicale, mais se réfère plutôt au verbe « injecter ». Ensemble, ces deux mots évoquent l’idée d’introduire du SQL dans une application web.
Introduire du SQL dans une application web… hmm… N’est-ce pas ce que nous faisons habituellement ? Oui, mais nous ne voulons pas qu’un pirate prenne le contrôle de notre base de données. Prenons un exemple pour mieux comprendre.
Imaginez que vous créez un site web PHP classique pour une boutique en ligne locale, et vous décidez d’ajouter un formulaire de contact comme celui-ci :
<form action="record_message.php" method="POST"> <label>Votre nom</label> <input type="text" name="name"> <label>Votre message</label> <textarea name="message" rows="5"></textarea> <input type="submit" value="Envoyer"> </form>
Supposons que le fichier send_message.php enregistre toutes les informations dans une base de données afin que les propriétaires de la boutique puissent lire les messages des utilisateurs ultérieurement. Le code pourrait ressembler à ceci :
<?php $name = $_POST['name']; $message = $_POST['message']; // vérifie si cet utilisateur a déjà un message mysqli_query($conn, "SELECT * from messages where name = $name"); // Autre code ici
Vous essayez donc d’abord de vérifier si cet utilisateur a déjà un message non lu. La requête SELECT * from messages where name = $name semble plutôt simple, n’est-ce pas ?
ERREUR !
Dans notre naïveté, nous avons ouvert les portes à une destruction instantanée de notre base de données. Pour que cela se produise, l’attaquant doit remplir les conditions suivantes :
- L’application utilise une base de données SQL (c’est le cas de presque toutes les applications aujourd’hui)
- La connexion actuelle à la base de données a les autorisations « modifier » et « supprimer » sur la base de données
- Les noms des tables importantes sont faciles à deviner
Le troisième point signifie que, maintenant que l’attaquant sait que vous gérez une boutique en ligne, il y a de fortes chances que vous stockiez les données de commande dans une table nommée ‘commandes’. Fort de ces informations, l’attaquant n’a plus qu’à saisir ceci comme nom :
Jean ; TRUNCATE commandes; ? Et voilà ! Voyons ce que devient la requête lorsqu’elle est exécutée par le script PHP :
SELECT * FROM messages WHERE nom = Jean ; TRUNCATE commandes;
Certes, la première partie de la requête comporte une erreur de syntaxe (pas de guillemets autour de « Jean »), mais le point-virgule force le moteur MySQL à interpréter une nouvelle instruction : TRUNCATE commandes. D’un seul coup, l’historique complet des commandes disparaît !
Maintenant que vous comprenez comment fonctionne l’injection SQL, voyons comment l’empêcher. Les deux conditions nécessaires pour qu’une injection SQL réussisse sont :
Comment prévenir l’injection SQL en PHP
Maintenant que les connexions à la base de données, les requêtes et les entrées utilisateur font partie de notre quotidien, comment pouvons-nous nous protéger de l’injection SQL ? Heureusement, c’est assez simple et il y a deux façons de procéder : 1) nettoyer les entrées utilisateur et 2) utiliser des requêtes préparées.
Assainir les entrées utilisateur
Si vous utilisez une ancienne version de PHP (5.5 ou antérieure, ce qui arrive souvent sur des hébergements mutualisés), il est conseillé de passer toutes les entrées utilisateur dans une fonction appelée mysql_real_escape_string(). Elle consiste à supprimer tous les caractères spéciaux d’une chaîne, de sorte 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 ‘Ceci est une chaîne’, le guillemet simple (‘) peut être utilisé par un attaquant pour manipuler la requête de base de données et provoquer une injection SQL. L’exécution de la fonction mysql_real_escape_string() produit ‘Ceci est une chaîne’, ajoutant une barre oblique inverse au guillemet simple, l’échappant. Ainsi, l’intégralité de la chaîne est désormais transmise à la base de données en toute sécurité, sans risque de manipulation des requêtes.
Cette approche a un inconvénient : c’est une technique très ancienne qui est liée aux anciennes méthodes d’accès aux bases de données en PHP. Depuis PHP 7, cette fonction n’existe plus, ce qui nous amène à notre solution suivante.
Utiliser des requêtes préparées
Les requêtes 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 directement la requête brute à la base de données, on lui indique d’abord la structure de la requête que nous allons envoyer. C’est ce que l’on appelle « préparer » une requête. 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 « remplir les trous » en reliant les entrées à la structure de requête que nous avons envoyée auparavant. Cela supprime tout pouvoir potentiel des entrées, les traitant comme de simples variables (ou charges utiles, si vous préférez) dans l’ensemble du processus. Voici un exemple d’utilisation de requêtes préparées :
<?php $servername = "localhost"; $username = "nom_utilisateur"; $password = "mot_de_passe"; $dbname = "ma_base_de_donnees"; // Créer une connexion $conn = new mysqli($servername, $username, $password, $dbname); // Vérifier la connexion if ($conn->connect_error) { die("La connexion a échoué: " . $conn->connect_error); } // préparer et lier $stmt = $conn->prepare("INSERT INTO MesInvités (prenom, nom, email) VALUES (?, ?, ?)"); $stmt->bind_param("sss", $prenom, $nom, $email); // définir les paramètres et exécuter $prenom = "Jean"; $nom = "Dupont"; $email = "jean.dupont@example.com"; $stmt->execute(); $prenom = "Marie"; $nom = "Martin"; $email = "marie.martin@example.com"; $stmt->execute(); $prenom = "Julie"; $nom = "Dubois"; $email = "julie.dubois@example.com"; $stmt->execute(); echo "Nouveaux enregistrements créés avec succès"; $stmt->close(); $conn->close(); ?>
Je sais que le processus semble inutilement complexe si vous êtes novice en matière de requêtes préparées, mais le concept en vaut la peine. Voici une excellente introduction à ce sujet.
Pour ceux qui sont déjà familiers avec l’extension PDO de PHP et qui l’utilisent pour créer des requêtes préparées, j’ai un petit conseil.
Attention : Soyez prudent lors de la configuration de PDO
Lors de l’utilisation de PDO pour accéder à la base de données, on peut se laisser aller à un faux sentiment de sécurité. « Ah, j’utilise PDO, je n’ai plus besoin de penser à rien d’autre » — c’est souvent le raisonnement. Il est vrai que PDO (ou les requêtes préparées MySQLi) est suffisant pour éviter toutes sortes d’attaques par injection SQL, mais il faut être prudent lors de sa configuration. Il est courant de simplement copier-coller du code de tutoriels 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 paramètre indique à PDO d’émuler les requêtes préparées au lieu d’utiliser la fonctionnalité réelle de la base de données. En conséquence, PHP envoie des chaînes de requête simples à la base de données même si votre code semble créer des requêtes préparées et définir des paramètres, etc. Autrement dit, 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 les requêtes préparées au niveau de la base de données, empêchant toute forme d’injection SQL.
Empêcher l’utilisation de WAF
Saviez-vous que vous pouviez également protéger les applications web contre l’injection SQL en utilisant un WAF (pare-feu applicatif web) ?
En fait, 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 de données, etc. Vous pouvez utiliser des solutions auto-hébergées comme Mod Security ou des solutions cloud.
Injection SQL et frameworks PHP modernes
L’injection SQL est tellement répandue, simple à réaliser, frustrante et dangereuse que tous les frameworks web PHP modernes intègrent des mesures de protection. Dans WordPress, par exemple, nous avons la fonction $wpdb->prepare(), tandis que si vous utilisez un framework MVC, il fait tout le travail pénible pour vous et vous n’avez même pas à penser à empêcher l’injection SQL. Il est un peu agaçant que dans WordPress, il faille préparer explicitement les requêtes, mais c’est WordPress après tout. 🙂
En d’autres termes, les développeurs web d’aujourd’hui n’ont plus à se soucier de l’injection SQL et ne sont même pas conscients de cette possibilité. Ainsi, même s’ils laissent une faille de sécurité dans leur application (par exemple, un paramètre de requête $_GET et une vieille habitude de déclencher une requête non protégée), les conséquences peuvent être catastrophiques. Il est donc toujours préférable de prendre le temps d’approfondir les fondations.
Conclusion
L’injection SQL est une attaque très désagréable sur une application web, mais elle est facile à éviter. Comme nous l’avons vu dans cet article, il suffit d’être prudent lors du traitement des entrées utilisateur (d’ailleurs, l’injection SQL n’est pas la seule menace liée à la gestion des entrées utilisateur) et lors de l’interrogation de la base de données. Cela dit, nous ne travaillons pas toujours dans le cadre d’un framework web, il est donc essentiel d’être conscient de ce type d’attaque et de ne pas tomber dans le piège.