2773 votes

Comment prévenir l'injection SQL en PHP ?

Si l'entrée de l'utilisateur est insérée sans modification dans une requête SQL, alors l'application devient vulnérable aux éléments suivants Injection SQL comme dans l'exemple suivant :

$unsafe_variable = $_POST['user_input']; 

mysql_query("INSERT INTO `table` (`column`) VALUES ('$unsafe_variable')");

C'est parce que l'utilisateur peut entrer quelque chose comme value'); DROP TABLE table;-- et la requête devient :

INSERT INTO `table` (`column`) VALUES('value'); DROP TABLE table;--')

Que peut-on faire pour éviter que cela ne se produise ?

9362voto

Theo Points 60103

Le site correct Le meilleur 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 de sorte que les données restent des données et seront ne jamais être interprété comme des 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, vous risquez de ne pas être en mesure d'accéder aux données. entièrement comprendre les détails, vous devez toujours utiliser des déclarations préparées et des requêtes paramétrées. Il s'agit d'instructions SQL qui sont envoyées au serveur de base de données et analysées par celui-ci, indépendamment de tout paramètre. De cette façon, il est impossible pour un attaquant d'injecter du SQL malveillant.

Vous avez essentiellement deux possibilités pour y parvenir :

  1. Utilisation de AOP (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
    }
  2. Utilisation de 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() y pg_execute() pour PostgreSQL). PDO est l'option universelle.


Configurer correctement la connexion

AOP

Notez que lorsque vous utilisez AOP pour accéder à une base de données MySQL réel les déclarations préparées sont non utilisé 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 en utilisant AOP est :

$dbConnection = new PDO('mysql:dbname=dbtest;host=127.0.0.1;charset=utf8mb4', 'user', 'password');

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

Dans l'exemple ci-dessus, le mode d'erreur n'est pas strictement nécessaire, mais il est conseillé de l'ajouter . De cette façon, PDO vous informera de toutes les erreurs MySQL en lançant l'icône PDOException .

Qu'est-ce que obligatoire Cependant, c'est le premier setAttribute() qui indique à PDO de désactiver les instructions préparées émulées et d'utiliser l'interface de l'utilisateur. réel les déclarations préparées. Cela permet de s'assurer que l'instruction et les valeurs ne sont pas analysées par PHP avant d'être envoyées au serveur MySQL (ce qui ne laisse aucune chance à un éventuel attaquant d'injecter du 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) ignorait silencieusement le paramètre charset dans le DSN.

Mysqli

Pour mysqli, nous devons suivre la même routine :

mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT); // error reporting
$dbConnection = new mysqli('127.0.0.1', 'username', 'password', 'test');
$dbConnection->set_charset('utf8mb4'); // charset

Explication

L'instruction SQL que vous passez à prepare est analysé et compilé par le serveur de la 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 la base de données l'endroit sur lequel vous voulez filtrer. Ensuite, lorsque vous appelez execute l'instruction préparée est combinée avec les valeurs de paramètres que vous spécifiez.

L'important ici est que les valeurs des paramètres sont combinées avec l'instruction compilée, et non une chaîne SQL. L'injection SQL fonctionne en incitant le script à inclure des chaînes malveillantes lorsqu'il crée le 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 traités comme des chaînes de caractères (bien que le moteur de la base de données puisse procéder à une certaine optimisation, de sorte que les paramètres peuvent aussi se retrouver sous forme de nombres, bien entendu). Dans l'exemple ci-dessus, si le paramètre $name La variable contient 'Sarah'; DELETE FROM employees le résultat serait simplement une recherche de la chaîne de caractères "'Sarah'; DELETE FROM employees" et vous ne vous retrouverez pas avec une table vide .

Un autre avantage de l'utilisation des 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 permet de gagner en rapidité.

Oh, et puisque vous avez demandé comment le 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 des requêtes dynamiques ?

Bien que vous puissiez toujours utiliser des instructions préparées pour les paramètres de la requête, la structure de la requête dynamique elle-même ne peut pas être paramétrée et certaines caractéristiques de la 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 restreint les valeurs possibles.

// Value whitelist
// $dir can only be 'DESC', otherwise it will be 'ASC'
if (empty($dir) || $dir !== 'DESC') {
   $dir = 'ASC';
}

49 votes

De plus, la documentation officielle de mysql_query ne permet d'exécuter qu'une seule requête, donc toute autre requête que ; est ignorée. Même si c'est déjà déprécié, il y a beaucoup de systèmes sous PHP 5.5.0 qui peuvent utiliser cette fonction. php.net/manual/fr/function.mysql-query.php

15 votes

C'est une mauvaise habitude mais c'est une solution post-problème : Non seulement pour l'injection SQL mais pour tout type d'injections (par exemple il y avait un trou d'injection de modèle de vue dans le framework F3 v2) si vous avez un vieux site web ou une application qui souffre de défauts d'injection, une solution est de réassigner les valeurs de vos variables prédéfinies supperglobales comme $_POST avec des valeurs échappées au démarrage. Avec PDO, il est encore possible d'échapper (aussi pour les frameworks actuels) : substr($pdo->quote($str, \PDO ::PARAM_STR), 1, -1)

21 votes

Cette réponse manque d'explications sur ce qu'est une déclaration préparée - une chose - c'est une perte de performance si vous utilisez beaucoup de déclarations préparées pendant votre requête et parfois cela représente une perte de performance de 10x. La meilleure solution serait d'utiliser PDO avec la liaison des paramètres désactivée, mais la préparation des déclarations désactivée.

1704voto

Matt Sheppard Points 32256

Pour utiliser la requête paramétrée, vous devez utiliser soit Mysqli, soit PDO. Pour réécrire votre exemple avec mysqli, nous aurions besoin de quelque chose comme ce qui suit.

<?php
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
$mysqli = new mysqli("server", "username", "password", "database_name");

$variable = $_POST["user-input"];
$stmt = $mysqli->prepare("INSERT INTO table (column) VALUES (?)");
// "s" means the database expects a string
$stmt->bind_param("s", $variable);
$stmt->execute();

La fonction clé sur laquelle vous devez vous renseigner est la suivante mysqli::prepare .

De plus, comme d'autres l'ont suggéré, vous trouverez peut-être utile/plus facile de passer à un niveau d'abstraction supérieur avec quelque chose comme AOP .

Veuillez noter que le cas que vous avez demandé est assez simple et que des cas plus complexes peuvent nécessiter des approches plus complexes. En particulier :

  • Si vous souhaitez modifier la structure du SQL en fonction de l'entrée de l'utilisateur, les requêtes paramétrées ne vous seront d'aucune aide, et l'échappement requis n'est pas couvert par l'option mysql_real_escape_string . Dans ce genre de cas, il est préférable de faire passer l'entrée de l'utilisateur par une liste blanche pour s'assurer que seules les valeurs "sûres" sont autorisées.

1 votes

En utilisant mysql_real_escape_string est suffisant ou je dois aussi utiliser le paramétrage ?

9 votes

@peimanF. Gardez une bonne habitude d'utiliser des requêtes paramétrées, même sur un projet local. Avec les requêtes paramétrées, vous êtes garanti qu'il n'y aura pas d'injection SQL. Mais gardez à l'esprit que vous devez assainir les données pour éviter les récupérations erronées (c'est-à-dire l'injection XSS, comme l'insertion de code HTML dans un texte) avec htmlentities par exemple

2 votes

@peimanF. Bonne pratique pour les requêtes paramétrées et les valeurs liées, mais la vraie chaîne d'échappement est bonne pour le moment.

1125voto

Chaque réponse ici ne couvre qu'une partie du problème. En fait, il y a quatre différentes parties de requête que l'on peut ajouter au SQL de manière dynamique : -

  • une chaîne
  • un nombre
  • un identifiant
  • un mot clé syntaxique

Et les déclarations préparées ne couvrent que deux d'entre elles.

Mais parfois, nous devons rendre notre requête encore plus dynamique, en ajoutant des opérateurs ou des identificateurs. Nous aurons donc besoin de techniques de protection différentes.

En général, une telle approche de protection est basée sur liste blanche .

Dans ce cas, chaque paramètre dynamique doit être codé en dur dans votre script et choisi dans cet ensemble. Par exemple, pour faire une commande dynamique :

$orders  = array("name", "price", "qty"); // Field names
$key = array_search($_GET['sort'], $orders)); // if we have such a name
$orderby = $orders[$key]; // If not, first one will be set automatically. 
$query = "SELECT * FROM `table` ORDER BY $orderby"; // Value is safe

Pour faciliter le processus, j'ai écrit un fonction d'aide à la liste blanche qui fait tout le travail en une seule ligne :

$orderby = white_list($_GET['orderby'], "name", ["name","price","qty"], "Invalid field name");
$query  = "SELECT * FROM `table` ORDER BY `$orderby`"; // sound and safe

Il existe un autre moyen de sécuriser les identifiants - l'échappement - mais je préfère m'en tenir à la liste blanche, qui constitue une approche plus robuste et plus explicite. Cependant, tant que vous avez un identifiant cité, vous pouvez échapper au caractère de citation pour le rendre sûr. Par exemple, par défaut pour mysql, vous devez doublez le caractère quote pour l'échapper . Les règles d'échappement sont différentes pour les autres SGBD.

Cependant, il y a un problème avec les mots-clés de la syntaxe SQL (tels que AND , DESC et autres), mais l'inscription sur liste blanche semble être la seule approche possible dans ce cas.

Une recommandation générale peut donc être formulée comme suit

  • Toute variable qui représente un littéral de données SQL (ou, pour faire simple, une chaîne SQL ou un nombre) doit être ajoutée par le biais d'une instruction préparée. Aucune exception.
  • Toute autre partie de la requête, telle qu'un mot clé SQL, un nom de table ou de champ, ou un opérateur, doit être filtrée par une liste blanche.

Mise à jour

Bien qu'il y ait un accord général sur les meilleures pratiques en matière de protection contre les injections SQL, il y a encore beaucoup de mauvaises pratiques. Et certaines d'entre elles sont trop profondément ancrées dans l'esprit des utilisateurs de PHP. Par exemple, sur cette même page, il y a (bien qu'invisible pour la plupart des visiteurs) plus de 80 réponses supprimées - toutes supprimées par la communauté en raison de leur mauvaise qualité ou de la promotion de pratiques mauvaises et dépassées. Pire encore, certaines des mauvaises réponses ne sont pas supprimées, mais prospèrent au contraire.

Par exemple, là (1) sont(2) encore(3) beaucoup(4) réponses(5) y compris le deuxième réponse la plus votée vous suggère d'utiliser l'échappement manuel des chaînes de caractères, une approche dépassée dont il est prouvé qu'elle n'est pas sûre.

Ou il y a une réponse légèrement meilleure qui suggère juste une autre méthode de formatage des chaînes de caractères et s'en vante même comme de la panacée ultime. Alors que, bien sûr, ce n'est pas le cas. Cette méthode n'est pas meilleure que le formatage ordinaire des chaînes de caractères, mais elle en conserve tous les inconvénients : elle ne s'applique qu'aux chaînes de caractères et, comme tout autre formatage manuel, il s'agit essentiellement d'une mesure facultative, non obligatoire, sujette à toutes sortes d'erreurs humaines.

Je pense que tout cela à cause d'une très vieille superstition, soutenue par des autorités telles que OWASP o le manuel PHP qui proclame l'égalité entre tout ce qui est "échappement" et la protection contre les injections SQL.

En dépit de ce que le manuel de PHP dit depuis des lustres, *`_escape_string` ne sécurise en aucun cas les données** et n'a jamais eu l'intention de le faire. En plus d'être inutile pour toute partie du SQL autre que la chaîne de caractères, l'échappement manuel est erroné, car il est manuel par opposition à automatisé.

Et l'OWASP aggrave encore la situation en insistant sur l'échappement. entrée utilisateur ce qui est un non-sens total : il ne devrait pas y avoir de tels mots dans le contexte de la protection contre les injections. Toute variable est potentiellement dangereuse, quelle que soit sa source ! Ou, en d'autres termes, chaque variable doit être correctement formatée pour être insérée dans une requête, quelle que soit la source. C'est la destination qui compte. Dès qu'un développeur commence à séparer les moutons des chèvres (en se demandant si telle ou telle variable est "sûre" ou non), il fait le premier pas vers le désastre. Sans parler du fait que même la formulation suggère l'échappement en vrac au point d'entrée, ressemblant à la fonction magique des guillemets - déjà méprisée, dépréciée et supprimée.

Donc, contrairement à ce qui se passe avec les "échappées", les déclarations préparées est la mesure qui protège effectivement contre l'injection SQL (le cas échéant).

0 votes

J'aime l'argument de l'"automatisation", qui protégera même un débutant contre une mauvaise entrée (il peut s'agir d'autres problèmes, comme vous l'avez dit, mais pas de celui-là), sans qu'il ait à réfléchir. C'est la voie à suivre. Cependant, et en se rappelant votre ancienne réponse Si chaque requête est soigneusement construite et que les entrées sont échappées, dans une base de données entièrement utf8mb4, avec php >= 7.2, MySQL >= 5.7, je cherche toujours une attaque par injection sql qui fonctionne dans ce contexte...

0 votes

@Breakingnotsobad se demande pourquoi vous cherchez ça mais ici ce n'est pas le bon endroit. Ici, nous parlons de comment sécuriser l'ensemble de votre application, pas comment la casser ou comment protéger une seule requête particulièrement sélectionnée et "soigneusement construite".

0 votes

"Je me demande pourquoi, parce que j'aimerais voir une preuve de concept qu'une chaîne correctement échappée peut être utilisée de manière abusive dans cet environnement. Il est de la responsabilité de chacun de fournir des requêtes sécurisées, et, encore une fois, toujours utiliser des instructions préparées est le moyen le plus sûr "automatisé" pour tous les développeurs. Cependant, peut-être qu'ici n'est pas le bon endroit, mais, juste pour dire, je n'ai pas encore vu quelqu'un fournir un PoC d'une telle attaque dans un environnement moderne...

876voto

Kibbee Points 36474

Je recommande d'utiliser AOP (PHP Data Objects) pour exécuter des requêtes SQL paramétrées.

Cela permet non seulement de se protéger contre les injections SQL, mais aussi d'accélérer les requêtes.

Et en utilisant PDO plutôt que mysql_ , mysqli_ y pgsql_ vous rendez votre application un peu plus abstraite de la base de données, dans le cas rare où vous devriez changer de fournisseur de base de données.

6 votes

Cette réponse est trompeuse. L'AOP n'est pas une baguette magique qui protège vos requêtes par une simple présence. Vous devez remplacer chaque variable de votre requête par un caractère de remplacement pour obtenir une protection de l'AOP.

651voto

Imran Points 20117

Utilisez PDO et les requêtes préparées.

( $conn est un PDO objet)

$stmt = $conn->prepare("INSERT INTO tbl VALUES(:id, :name)");
$stmt->bindValue(':id', $id);
$stmt->bindValue(':name', $name);
$stmt->execute();

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X