Le correct le moyen d'éviter les attaques par injection SQL, quelle que soit la base de données que vous utilisez, est de séparer les données de SQL , afin que les données restent des données et ne soient jamais interprétées sous forme de commandes par l'analyseur SQL. Il est possible de créer une instruction SQL avec des parties de données correctement formatées, mais si vous ne le faites pas entièrement comprendre les détails, vous devez toujours utiliser des instructions préparées et des requêtes paramétrées. Il s'agit d'instructions SQL qui sont envoyées et analysées par le serveur de base de données séparément de tous les paramètres. De cette façon, il est impossible pour un attaquant d'injecter du code SQL malveillant.
Vous avez essentiellement deux options pour y parvenir :
-
Utilisation de PDO (pour tout pilote de base de données pris en charge) :
$stmt = $pdo->prepare('SELECT * FROM employees WHERE name = :name'); $stmt->execute([ 'name' => $name ]); foreach ($stmt as $row) { // Do something with $row }
-
Utiliser MySQLi (pour MySQL) :
$stmt = $dbConnection->prepare('SELECT * FROM employees WHERE name = ?'); $stmt->bind_param('s', $name); // 's' specifies the variable type => 'string' $stmt->execute(); $result = $stmt->get_result(); while ($row = $result->fetch_assoc()) { // Do something with $row }
Si vous vous connectez à une base de données autre que MySQL, il existe une deuxième option spécifique au pilote à laquelle vous pouvez vous référer (par exemple, pg_prepare()
et pg_execute()
pour PostgreSQL). PDO est l'option universelle.
Configurer correctement la connexion
Notez que lors de l'utilisation de PDO pour accéder à une base de données MySQL réelle les instructions préparées ne sont pas utilisées par défaut . Pour résoudre ce problème, vous devez désactiver l'émulation des instructions préparées. Un exemple de création d'une connexion à l'aide de PDO est :
$dbConnection = new PDO('mysql:dbname=dbtest;host=127.0.0.1;charset=utf8', 'user', 'password');
$dbConnection->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$dbConnection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
Dans l'exemple ci-dessus, le mode erreur n'est pas strictement nécessaire, mais il est conseillé de l'ajouter . De cette façon, le script ne s'arrêtera pas avec une Fatal Error
quand quelque chose ne va pas. Et cela donne au développeur la possibilité de catch
toute erreur(s) qui sont throw
n comme PDOException
s.
Qu'est-ce qui est obligatoire ? , cependant, est le premier setAttribute()
ligne, qui indique à PDO de désactiver les instructions préparées émulées et d'utiliser real déclarations préparées. Cela garantit que l'instruction et les valeurs ne sont pas analysées par PHP avant de l'envoyer au serveur MySQL (ce qui ne laisse aucune chance à un éventuel attaquant d'injecter du code SQL malveillant).
Bien que vous puissiez définir le charset
dans les options du constructeur, il est important de noter que les "anciennes" versions de PHP (avant 5.3.6) ignorer silencieusement le paramètre charset
dans la DSN.
Explication
L'instruction SQL que vous transmettez à prepare
est analysé et compilé par le serveur de base de données. En spécifiant des paramètres (soit un ?
ou un paramètre nommé comme :name
dans l'exemple ci-dessus), vous indiquez au moteur de base de données où vous souhaitez filtrer. Ensuite, lorsque vous appelez execute
, l'instruction préparée est combinée avec les valeurs de paramètre que vous spécifiez.
L'important ici est que les valeurs des paramètres soient combinées avec l'instruction compilée, et non avec une chaîne SQL. L'injection SQL fonctionne en incitant le script à inclure des chaînes malveillantes lorsqu'il crée du SQL à envoyer à la base de données. Ainsi, en envoyant le SQL réel séparément des paramètres, vous limitez le risque de vous retrouver avec quelque chose que vous n'aviez pas prévu.
Tous les paramètres que vous envoyez lors de l'utilisation d'une instruction préparée seront simplement traités comme des chaînes (bien que le moteur de base de données puisse effectuer une certaine optimisation afin que les paramètres puissent également se retrouver sous forme de nombres, bien sûr). Dans l'exemple ci-dessus, si le $name
la variable contient 'Sarah'; DELETE FROM employees
le résultat serait simplement une recherche de la chaîne "'Sarah'; DELETE FROM employees"
, et vous ne vous retrouverez pas avec une table vide
.
Un autre avantage de l'utilisation d'instructions préparées est que si vous exécutez la même instruction plusieurs fois dans la même session, elle ne sera analysée et compilée qu'une seule fois, ce qui vous donnera des gains de vitesse.
Oh, et puisque vous avez demandé comment faire pour un insert, voici un exemple (en utilisant PDO) :
$preparedStatement = $db->prepare('INSERT INTO table (column) VALUES (:column)');
$preparedStatement->execute([ 'column' => $unsafeValue ]);
Les instructions préparées peuvent-elles être utilisées pour les requêtes dynamiques ?
Bien que vous puissiez toujours utiliser des instructions préparées pour les paramètres de requête, la structure de la requête dynamique elle-même ne peut pas être paramétrée et certaines fonctionnalités de requête ne peuvent pas être paramétrées.
Pour ces scénarios spécifiques, la meilleure chose à faire est d'utiliser un filtre de liste blanche qui limite les valeurs possibles.
// Value whitelist
// $dir can only be 'DESC', otherwise it will be 'ASC'
if (empty($dir) || $dir !== 'DESC') {
$dir = 'ASC';
}