Question Les instructions préparées par PDO sont-elles suffisantes pour empêcher l'injection SQL?


Disons que j'ai un code comme celui-ci:

$dbh = new PDO("blahblah");

$stmt = $dbh->prepare('SELECT * FROM users where username = :username');
$stmt->execute( array(':username' => $_REQUEST['username']) );

La documentation PDO dit:

Les paramètres des instructions préparées n'ont pas besoin d'être quotés; le conducteur le gère pour vous.

Est-ce vraiment tout ce que j'ai besoin de faire pour éviter les injections SQL? est-ce vraiment si facile?

Vous pouvez supposer MySQL si cela fait une différence. En outre, je ne suis vraiment curieux que de l'utilisation d'instructions préparées contre l'injection SQL. Dans ce contexte, je ne me soucie pas de XSS ou d'autres vulnérabilités possibles.


548
2017-09-25 15:43


origine


Réponses:


La réponse courte est NON, PDO prépare ne vous protégera pas de toutes les attaques SQL-Injection possibles. Pour certains cas-bords obscurs.

Je m'adapte cette réponse parler de PDO ...

La longue réponse n'est pas si facile. C'est basé sur une attaque démontré ici.

L'attaque

Alors, commençons par montrer l'attaque ...

$pdo->query('SET NAMES gbk');
$var = "\xbf\x27 OR 1=1 /*";
$query = 'SELECT * FROM test WHERE name = ? LIMIT 1';
$stmt = $pdo->prepare($query);
$stmt->execute(array($var));

Dans certaines circonstances, cela retournera plus d'une rangée. Disons ce qui se passe ici:

  1. Sélection d'un jeu de caractères

    $pdo->query('SET NAMES gbk');
    

    Pour que cette attaque fonctionne, nous avons besoin du codage que le serveur attend de la connexion pour encoder ' comme en ASCII, c'est-à-dire 0x27  et avoir un caractère dont le dernier octet est un ASCII \ c'est à dire. 0x5c. Il s'avère que 5 encodages de ce type sont supportés par défaut dans MySQL 5.6: big5, cp932, gb2312, gbk et sjis. Nous allons sélectionner gbk ici.

    Maintenant, il est très important de noter l'utilisation de SET NAMES ici. Cela définit le jeu de caractères SUR LE SERVEUR. Il y a une autre façon de le faire, mais nous y arriverons assez tôt.

  2. La charge utile

    La charge utile que nous allons utiliser pour cette injection commence par la séquence d'octets 0xbf27. Dans gbk, c'est un caractère multi-octet invalide; dans latin1c'est la ficelle ¿'. Notez que dans latin1  et  gbk, 0x27 seul est littéral ' personnage.

    Nous avons choisi cette charge parce que, si nous avons appelé addslashes() dessus, nous insérerions un ASCII \ c'est à dire. 0x5c, avant le ' personnage. Nous nous retrouverions avec 0xbf5c27, qui dans gbk est une séquence de deux caractères: 0xbf5c suivi par 0x27. Ou en d'autres termes, un valide caractère suivi d'un non échappé '. Mais nous n'utilisons pas addslashes(). Donc, à la prochaine étape ...

  3. $ stmt-> execute ()

    La chose importante à réaliser ici est que PDO par défaut NE PAS faites de vraies déclarations préparées. Il les émule (pour MySQL). Par conséquent, PDO génère en interne la chaîne de requête, appelant mysql_real_escape_string() (la fonction MySQL C API) sur chaque valeur de chaîne liée.

    L'appel de l'API C à mysql_real_escape_string() diffère de addslashes() en ce qu'il connaît le jeu de caractères de connexion. Il peut donc effectuer l'échappement correctement pour le jeu de caractères attendu par le serveur. Cependant, jusqu'à présent, le client pense que nous utilisons encore latin1 pour la connexion, parce que nous ne l'avons jamais dit autrement. Nous avons dit au serveur nous utilisons gbk, mais le client pense toujours qu'il est latin1.

    Par conséquent, l'appel à mysql_real_escape_string() insère le backslash, et nous avons une suspension libre ' personnage dans notre contenu "échappé"! En fait, si nous devions regarder $var dans le gbk jeu de caractères, nous verrons:

    縗 'OU 1 = 1 / *

    Ce qui est exactement ce que l'attaque nécessite.

  4. La requête

    Cette partie est juste une formalité, mais voici la requête rendue:

    SELECT * FROM test WHERE name = '縗' OR 1=1 /*' LIMIT 1
    

Félicitations, vous venez d'attaquer avec succès un programme en utilisant PDO Prepared Statements ...

Le correctif simple

Maintenant, il est intéressant de noter que vous pouvez empêcher cela en désactivant les instructions préparées émulées:

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

Cette volonté d'habitude résulte en une instruction préparée réelle (c'est-à-dire que les données sont envoyées dans un paquet distinct de la requête). Cependant, sachez que PDO sera silencieusement se retirer à émuler des déclarations que MySQL ne peut pas préparer nativement: ceux qui peuvent être listé dans le manuel, mais attention à sélectionner la version de serveur appropriée).

Le correctif correct

Le problème ici est que nous n'avons pas appelé l'API C mysql_set_charset() au lieu de SET NAMES. Si nous le faisions, tout irait bien si nous utilisions une version de MySQL depuis 2006.

Si vous utilisez une version antérieure de MySQL, alors punaise dans mysql_real_escape_string() signifie que les caractères multi-octets invalides tels que ceux de notre charge utile ont été traités comme des octets uniques à des fins d'échappement même si le client a été correctement informé de l'encodage de la connexion et ainsi cette attaque réussirait toujours. Le bug a été corrigé dans MySQL 4.1.20, 5.0.22 et 5.1.11.

Mais le pire est que PDO n'a pas exposé l'API C pour mysql_set_charset() jusqu'à 5.3.6, donc dans les versions antérieures, il ne peux pas empêcher cette attaque pour toutes les commandes possibles!  Il est maintenant exposé en tant que Paramètre DSN, qui devrait être utilisé au lieu de  SET NAMES...

La grâce salvatrice

Comme nous l'avons dit au début, pour que cette attaque fonctionne, la connexion à la base de données doit être encodée en utilisant un jeu de caractères vulnérable. utf8mb4 est pas vulnérable et peut encore soutenir chaque Caractère Unicode: vous pouvez donc l'utiliser à la place, mais il n'est disponible que depuis MySQL 5.5.3. Une alternative est utf8, qui est aussi pas vulnérable et peut supporter l'ensemble de l'Unicode Plan multilingue de base.

Alternativement, vous pouvez activer NO_BACKSLASH_ESCAPES Mode SQL, qui (entre autres choses) modifie le fonctionnement de mysql_real_escape_string(). Avec ce mode activé, 0x27 sera remplacé par 0x2727 plutôt que 0x5c27 et ainsi le processus d'échappement ne peux pas créer des caractères valides dans l'un des codages vulnérables où ils n'existaient pas auparavant (c.-à-d. 0xbf27 est encore 0xbf27 etc.) - de sorte que le serveur rejettera toujours la chaîne comme invalide. Cependant, voir @ réponse eggyal pour une vulnérabilité différente qui peut résulter de l'utilisation de ce mode SQL (mais pas avec PDO).

Exemples sûrs

Les exemples suivants sont sûrs:

mysql_query('SET NAMES utf8');
$var = mysql_real_escape_string("\xbf\x27 OR 1=1 /*");
mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1");

Parce que le serveur attend utf8...

mysql_set_charset('gbk');
$var = mysql_real_escape_string("\xbf\x27 OR 1=1 /*");
mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1");

Parce que nous avons correctement défini le jeu de caractères pour que le client et le serveur correspondent.

$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$pdo->query('SET NAMES gbk');
$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(array("\xbf\x27 OR 1=1 /*"));

Parce que nous avons désactivé les instructions préparées émulées.

$pdo = new PDO('mysql:host=localhost;dbname=testdb;charset=gbk', $user, $password);
$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(array("\xbf\x27 OR 1=1 /*"));

Parce que nous avons correctement défini le jeu de caractères.

$mysqli->query('SET NAMES gbk');
$stmt = $mysqli->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$param = "\xbf\x27 OR 1=1 /*";
$stmt->bind_param('s', $param);
$stmt->execute();

Parce que MySQLi fait de vraies instructions préparées tout le temps.

Emballer

Si vous:

  • Utilisez les versions modernes de MySQL (fin 5.1, tous les 5.5, 5.6, etc) ET Paramètre de jeu de caractères DSN de PDO (en PHP ≥ 5.3.6)

OU

  • N'utilisez pas de jeu de caractères vulnérable pour l'encodage de connexion (vous utilisez uniquement utf8 / latin1 / ascii / etc)

OU

  • Activer NO_BACKSLASH_ESCAPES Mode SQL

Vous êtes sûr à 100%.

Sinon, vous êtes vulnérable même si vous utilisez des instructions préparées PDO ...

Addenda

J'ai travaillé lentement sur un patch pour changer le défaut de ne pas émuler prépare pour une future version de PHP. Le problème que je rencontre est que beaucoup de tests cassent quand je fais cela. Un problème est que les préparations émulées ne lancent que des erreurs de syntaxe à l'exécution, mais que les vraies préparations vont lancer des erreurs sur la préparation. Cela peut donc causer des problèmes (et fait partie de la raison pour laquelle les tests sont en cours).


676
2017-08-30 17:22



Les instructions préparées / requêtes paramétrées sont généralement suffisantes pour empêcher 1er ordre injection sur cette déclaration*. Si vous utilisez SQL dynamique non vérifié ailleurs dans votre application, vous êtes toujours vulnérable à 2ème ordre injection.

L'injection de second ordre signifie que les données ont été traitées une fois dans la base de données avant d'être incluses dans une requête, et sont beaucoup plus difficiles à extraire. AFAIK, vous ne voyez presque jamais de vraies attaques de second ordre, car il est généralement plus facile pour les attaquants de s'introduire en société, mais vous avez parfois des bugs de second ordre en raison d'une bénignité supplémentaire. ' caractères ou similaires.

Vous pouvez effectuer une attaque par injection de second ordre lorsque vous pouvez provoquer le stockage d'une valeur dans une base de données qui est ensuite utilisée comme littéral dans une requête. Par exemple, supposons que vous entrez les informations suivantes en tant que nouveau nom d'utilisateur lors de la création d'un compte sur un site Web (en supposant que MySQL DB pour cette question):

' + (SELECT UserName + '_' + Password FROM Users LIMIT 1) + '

S'il n'y a pas d'autres restrictions sur le nom d'utilisateur, une instruction préparée garantit toujours que la requête intégrée ci-dessus ne s'exécute pas au moment de l'insertion et stocke la valeur correctement dans la base de données. Toutefois, imaginez que plus tard, l'application extrait votre nom d'utilisateur de la base de données et utilise la concaténation de chaîne pour inclure cette valeur dans une nouvelle requête. Vous pourriez voir le mot de passe de quelqu'un d'autre. Étant donné que les premiers noms des utilisateurs ont tendance à être des administrateurs, vous pouvez également avoir donné la ferme. (Notez également: c'est une raison de plus de ne pas stocker les mots de passe en texte clair!)

Nous voyons, alors, que les déclarations préparées sont suffisantes pour une seule requête, mais en eux-mêmes, ils sont ne pas suffisant pour se protéger contre les attaques par injection SQL tout au long d'une application entière, car il leur manque un mécanisme pour faire en sorte que tous les accès à une base de données au sein de l'application utilisent du code sécurisé. Cependant, utilisé dans le cadre d'une bonne conception d'application - qui peut inclure des pratiques telles que la révision de code ou l'analyse statique, ou l'utilisation d'un ORM, d'une couche de données ou d'une couche de service qui limite le SQL dynamique. déclarations préparées sont l'outil principal pour résoudre le problème d'injection Sql.


492
2017-09-25 15:50



Non, ils ne sont pas toujours.

Cela dépend si vous autorisez l'entrée de l'utilisateur à être placé dans la requête elle-même. Par exemple:

$dbh = new PDO("blahblah");

$tableToUse = $_GET['userTable'];

$stmt = $dbh->prepare('SELECT * FROM ' . $tableToUse . ' where username = :username');
$stmt->execute( array(':username' => $_REQUEST['username']) );

serait vulnérable aux injections SQL et l'utilisation d'instructions préparées dans cet exemple ne fonctionnerait pas, car l'entrée de l'utilisateur est utilisée comme un identifiant, pas comme une donnée. La bonne réponse ici serait d'utiliser une sorte de filtrage / validation comme:

$dbh = new PDO("blahblah");

$tableToUse = $_GET['userTable'];
$allowedTables = array('users','admins','moderators');
if (!in_array($tableToUse,$allowedTables))    
 $tableToUse = 'users';

$stmt = $dbh->prepare('SELECT * FROM ' . $tableToUse . ' where username = :username');
$stmt->execute( array(':username' => $_REQUEST['username']) );

Remarque: vous ne pouvez pas utiliser PDO pour lier des données qui sortent du langage DDL (Data Definition Language), c'est-à-dire que cela ne fonctionne pas:

$stmt = $dbh->prepare('SELECT * FROM foo ORDER BY :userSuppliedData');

La raison pour laquelle ce qui précède ne fonctionne pas est parce que DESC et ASC ne sont pas Les données. PDO ne peut échapper à Les données. Deuxièmement, vous ne pouvez même pas mettre ' citations autour d'elle. La seule façon de permettre le tri choisi par l'utilisateur est de filtrer manuellement et de vérifier que c'est soit DESC ou ASC.


38
2018-04-21 09:00



Oui, c'est suffisant. La façon dont les attaques de type injection fonctionnent, est en quelque sorte obtenir un interprète (La base de données) pour évaluer quelque chose, cela aurait dû être des données, comme si c'était du code. Cela n'est possible que si vous mélangez du code et des données dans le même support (par exemple, lorsque vous construisez une requête sous forme de chaîne).

Les requêtes paramétrées fonctionnent en envoyant le code et les données séparément, de sorte qu'il jamais être possible de trouver un trou dans cela.

Vous pouvez toujours être vulnérable à d'autres attaques de type injection. Par exemple, si vous utilisez les données dans une page HTML, vous pourriez être sujet à des attaques de type XSS.


23
2017-09-25 15:55



Non ce n'est pas suffisant (dans certains cas spécifiques)! Par défaut, PDO utilise des instructions préparées émulées lors de l'utilisation de MySQL en tant que pilote de base de données. Vous devez toujours désactiver les instructions préparées émulées lors de l'utilisation de MySQL et de PDO:

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

Une autre chose qui devrait toujours être fait, il définit le bon encodage de la base de données:

$dbh = new PDO('mysql:dbname=dbtest;host=127.0.0.1;charset=utf8', 'user', 'pass');

Voir aussi cette question connexe: Comment puis-je empêcher l'injection SQL en PHP?

Notez également que cela ne concerne que le côté de la base de données des choses que vous auriez encore à regarder vous-même lors de l'affichage des données. Par exemple. en utilisant htmlspecialchars() encore une fois avec le style d'encodage et de devis correct.


22
2017-08-30 17:00



Personnellement, j'utiliserais toujours une forme d'assainissement sur les données, car vous ne pouvez jamais faire confiance aux entrées utilisateur. Cependant, lorsque vous utilisez des espaces réservés / liaison de paramètres, les données entrées sont envoyées au serveur séparément à l'instruction sql puis reliées ensemble. La clé ici est que cela lie les données fournies à un type spécifique et une utilisation spécifique et élimine toute possibilité de modifier la logique de l'instruction SQL.


8
2017-09-25 15:50



Eaven si vous allez empêcher l'injection de SQL frontal, en utilisant les contrôles html ou js, vous devez considérer que les contrôles frontaux sont "bypassable".

Vous pouvez désactiver js ou modifier un modèle avec un outil de développement frontal (intégré avec firefox ou chrome de nos jours).

Donc, afin d'empêcher l'injection SQL, serait bon d'assainir le backend de date d'entrée à l'intérieur de votre contrôleur.

Je voudrais vous suggérer d'utiliser la fonction PHP native de filter_input () afin de désinfecter les valeurs GET et INPUT.

Si vous voulez aller de l'avant avec la sécurité, pour les requêtes de base de données sensibles, je voudrais vous suggérer d'utiliser l'expression régulière pour valider le format de données. preg_match () vous aidera dans ce cas! Mais prends soin de toi! Le moteur Regex n'est pas si léger. Utilisez-le seulement si nécessaire, sinon les performances de votre application vont diminuer.

La sécurité a un coût, mais ne gâchez pas votre performance!

Exemple facile:

si vous voulez vérifier si une valeur, reçue de GET est un nombre, moins de 99     if (! preg_match ('/ [0-9] {1,2} /')) {...} est plus lourd de

if (isset($value) && intval($value)) <99) {...}

Donc, la réponse finale est: "Non! PDO Prepared Statements n'empêche pas toute sorte d'injection sql"; Cela n'empêche pas les valeurs inattendues, juste une concaténation inattendue


-1
2018-03-04 20:17