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




types d injections sql (6)

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.


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 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 que DESC et ASC ne sont pas des données . Le PDO ne peut s'échapper que pour les données . Deuxièmement, vous ne pouvez même pas mettre ' citations autour. La seule façon de permettre le tri choisi par l'utilisateur est de filtrer manuellement et de vérifier qu'il s'agit de DESC ou ASC .


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.

J'adapte cette réponse pour parler de PDO ...

La longue réponse n'est pas si facile. C'est basé sur une attaque démontrée 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 de l'encodage que le serveur attend de la connexion pour encoder ' comme en ASCII soit 0x27 et avoir un caractère dont l'octet final est un ASCII \ soit 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. Ceci 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 . En gbk , c'est un caractère multi- gbk invalide; en latin1 , c'est la chaîne ¿' . Notez que dans latin1 et gbk , 0x27 est un caractère littéral.

    Nous avons choisi cette charge parce que, si nous addslashes() dessus, nous 0x5c un ASCII \ ie 0x5c , avant le caractère. Donc nous 0xbf5c27 avec 0xbf5c27 , qui dans gbk est une séquence de deux caractères: 0xbf5c suivi de 0x27 . Ou en d'autres termes, un caractère valide suivi d'un caractère 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 fait PAS de vraies instructions préparées. Il les émule (pour MySQL). Par conséquent, PDO mysql_real_escape_string() interne la chaîne de requête, en appelant mysql_real_escape_string() (la fonction API MySQL C) sur chaque valeur de chaîne liée.

    L'appel de l'API C vers mysql_real_escape_string() diffère de addslashes() en ce qu'il connaît le jeu de caractères de la 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, car nous ne l'avons jamais dit autrement. Nous avons dit au serveur que nous gbk , mais le client pense toujours que c'est latin1 .

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

    縗' OR 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);

Cela se traduira généralement par une véritable instruction préparée (c'est-à-dire que les données sont envoyées dans un paquet distinct de la requête). Cependant, sachez que PDO se fallback silencieusement sur les instructions d'émulation que MySQL ne peut pas préparer nativement: celles qui peuvent être listed dans le manuel, mais attention à sélectionner la version du serveur approprié).

Le correctif correct

Le problème ici est que nous n'avons pas appelé mysql_set_charset() l'API C 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 un bug dans mysql_real_escape_string() signifiait que les caractères multi-octets invalides tels que ceux de notre payload étaient traités comme des octets uniques à des fins d'échappement même si le client avait été correctement informé de l'encodage de la connexion . 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, c'est que PDO n'a pas exposé l'API C pour mysql_set_charset() avant 5.3.6, donc dans les versions précédentes, il ne peut pas empêcher cette attaque pour toutes les commandes possibles! Il est maintenant exposé en tant que paramètre DSN , qui devrait être utilisé à la place 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 n'est pas vulnérable et peut supporter tous les caractères Unicode: vous pouvez donc choisir de l'utiliser à la place - mais il n'est disponible que depuis MySQL 5.5.3. Une alternative est utf8 , qui n'est pas non plus vulnérable et peut prendre en charge l'intégralité du plan multilingue de base Unicode.

Alternativement, vous pouvez activer le mode SQL NO_BACKSLASH_ESCAPES , qui (entre autres choses) modifie l'opération 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 peut pas créer des caractères valides dans l'un des encodages vulnérables où ils n'existaient pas auparavant (ie 0xbf27 est toujours 0xbf27 etc.) - donc le serveur sera toujours rejeter la chaîne comme invalide. Cependant, voir la réponse de @ 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:

  • Utiliser les versions modernes de MySQL (5.1, 5.5, 5.6, etc.) et le 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 la connexion (utilisez uniquement utf8 / latin1 / ascii / etc)

OU

  • Activer le mode SQL NO_BACKSLASH_ESCAPES

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 lors de la préparation. Cela peut donc causer des problèmes (et fait partie de la raison pour laquelle les tests sont en cours).


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

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 verrez presque jamais de vraies attaques de deuxième 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 à cause de caractères supplémentaires bénins 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 vérifie 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. Cependant, 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 donc que les instructions préparées sont suffisantes pour une seule requête, mais qu'elles ne suffisent pas à protéger contre les attaques par injection SQL dans toute une application, car elles manquent de mécanisme pour garantir que tous les accès à une base de données utilisent l'application. code de sécurité 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 limitant les instructions SQL dynamiques. problème. Si vous suivez de bons principes de conception d'application, de sorte que votre accès aux données est séparé du reste de votre programme, il devient facile d'appliquer ou de vérifier que chaque requête utilise correctement le paramétrage. Dans ce cas, l'injection sql (premier et deuxième ordre) est complètement empêchée.

* Il s'avère que MySql / PHP sont (juste, étaient) simplement stupides sur la manipulation des paramètres lorsque des caractères larges sont impliqués, et il y a encore un cas rare décrit dans l' autre réponse hautement votée ici qui peut permettre à l'injection de glisser à travers un paramétré question.


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 à nouveau htmlspecialchars() avec le style d'encodage et de citations correct.


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.


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





sql-injection