Préparation des requêtes SQL

Introduction

Dans le travail pratique que nous avons effectué, la requête SQL suivante nécessitait la valeur de id_discussion pour pouvoir être exécutée :

./serveur/sql.js
var requete_temp = '\
  SELECT\
    COUNT(messages.id_message) AS "nombre_messages",\
    discussions.sujet,\
    discussions.id_discussion,\
    messages.nom_utilisateur AS "utilisateur_createur",\
    messages.date_ecriture\
  FROM discussions, messages\
  WHERE discussions.id_discussion = messages.id_discussion\
  AND messages.id_discussion = '
  /* ici, nous concaténons l'identifiant de la discussion à la
  requête SQL pour que celle-ci puisse être exécutée correctement */
  + results[i].id_discussion +
  'ORDER BY messages.date_ecriture\
  LIMIT 0,1;';

En concaténant la variable results[i].id_discussion à la chaîne de caractères requete_temp, nous avons obtenu une requête SQL “personnalisée” dans laquelle id_discussion changeait à chaque nouveau passage dans la boucle. Dans la suite de notre application, outre pour la sélection de données, nous aurons besoin de requêtes “personnalisées” pour l’insertion, la mise à jour et la suppression de données. Pour ce faire, nous pourrions utiliser la même méthode que ci-dessus ; par exemple, une requête pour insérer un nouveau message ressemblerait à ceci :

/* nous supposons que les variables nom_utilisateur, contenu,
date_ecriture, id_discussion et likes existent */
var requete_sql = '\
  INSERT INTO messages(nom_utilisateur, contenu, date_ecriture, id_discussion, likes)\
  VALUES("' + nom_utilisateur + '","' + contenu + '", NOW(),' + id_discussion + ', 0)';

Mais il en découlerait un problème important : ce code serait vulnérable aux injections SQL.

Question

Les injections SQL ? Qu’est-ce que c’est ?

Il s’agit de requêtes SQL que peut effectuer un utilisateur en injectant une requête dans le contenu d’un champ qu’il peut remplir à l’aide de l’application ; par exemple, un utilisateur pourrait faire une injection SQL en écrivant le message suivant :

Mouhaha, je peux faire planter cette application !
En voici la preuve... ", NOW(), 1, 0; DROP TABLE messages;

En publiant ce message, un utilisateur exécuterait la première requête SQL (qui ajoute son message dans la base de données), mais exécuterait ensuite une autre requête SQL, DROP TABLE messages, qui supprimerait la table messages.

Il va sans dire que cette technique permet également d’exécuter d’autres instructions SQL, allant de la récupération des données des utilisateurs à la suppression de la base de données.

Pour éviter des abus de ce type, il convient donc de préparer les requêtes SQL lorsqu’elles contiennent des données venant des utilisateurs. La préparation de requêtes permet d’éviter les désagréments des injections SQL en échappant tous les caractères qui pourraient corrompre la requête SQL. Dans une requête préparée, le message qui faisait planter le serveur auparavant est donc formaté de la manière suivante :

Mouhaha, je peux faire planter cette application !
En voici la preuve... \", NOW(), 1, 0; DROP TABLE messages;

Dans le cas présent, la préparation ajoute à la requête un antislash avant le guillemet, empêchant ainsi la fermeture de la chaîne de caractères contenant le message et, par conséquent, l’exécution des instructions qui suivent ce guillemet.

Code

Maintenant que nous avons vu les danger des injections SQL, regardons comment nous en prémunir.

node-mysql, le module NPM que nous utilisons pour dialoguer avec la base de données, fournit une fonction, mysql.format, qui permet de préparer les requêtes SQL. Celle-ci accepte deux paramètres ; le premier est la requête SQL, et le deuxième est un array qui contient les valeurs à ajouter à la requête.

Toutefois, cet outil comporte quelques bugs, notamment au niveau des accents graves (`) et des points (.). Pour résoudre ces problèmes, nous allons donc créer une nouvelle fonction dans sql.js

./serveur/sql.js
exports.preparer = function(mysql, requete_sql, inserts) {
  requete_sql = mysql.format(requete_sql, inserts)
    // nous utilisons la méthode .remplace avec une expression régulière
    // pour supprimer les accents graves et les points
    .replace(/`/g, "'")
    .replace(/'\.'/g, ".")
    .replace(/'/g, "\\'");
  return requete_sql;
}

Utilisation

Voyons comment utiliser sql.preparer() pour formater correctement nos requêtes. Dans cet exemple, nous allons reprendre notre requête SQL pour insérer un nouveau message mais, cette fois, en préparant la requête SQL :

var requete_sql = '\
  INSERT INTO messages(nom_utilisateur, contenu, date_ecriture, id_discussion, likes)'+
  // ici, les valeurs prennent "??" lorsque nous insérons une chaîne de caractères
  // et "?" lorsqu'il s'agit d'un nombre
  'VALUES(??, ??, NOW(), ?, 0)';
// l'array "inserts" contient, dans l'ordre d'apparition dans la requête, les éléments
// qui doivent remplacer les points d'interrogation
var inserts = [
  nom_utilisateur,
  contenu,
  id_discussion
];
// enfin, nous préparons la requête SQL en exécutant la fonction sql.preparer()
requete_sql = sql.preparer(mysql, requete_sql, inserts);

Et voilà ! Nous sommes désormais parés contre toute injection SQL.