Les déclarations préparées par AOP sont-elles suffisantes pour prévenir L'injection de 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 de L'AOP dit:

les paramètres des déclarations préparées n'ont pas besoin d'être cités; le pilote les gère pour vous.

est-ce vraiment tout ce que je dois faire pour éviter les injections de SQL? Est-ce vraiment si simple?

vous pouvez supposer MySQL si cela fait une différence. Aussi, je suis vraiment seulement curieux au sujet de l'utilisation de déclarations préparées contre l'injection de SQL. Dans ce contexte, Je ne me soucie pas de XSS ou d'autres vulnérabilités possibles.

567
demandé sur Patrick Hofman 2008-09-25 19:43:35

7 réponses

la réponse courte est Non , PDO prépare ne pas vous défendre de toutes les attaques SQL-Injection possibles. Pour certaines bordures obscures.

je suis l'adaptation de cette réponse pour parler de l'AOP...

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

L'Attaque

alors, commençons en montrant 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, qui retournera plus d'une rangée. Voyons 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 sur la connexion à la fois pour encoder ' comme en ASCII i.e. 0x27 et pour avoir un caractère dont le octet final est un ASCII \ i.e. 0x5c . Comme il s'avère, il y a 5 codages de ce type pris en charge dans MySQL 5.6 par défaut: big5 , cp932 , gb2312 , gbk et sjis . Nous sélectionnerons gbk ici.

    maintenant, il est très important de noter l'utilisation de SET NAMES ici. Ce paramètre définit le jeu de caractères sur le serveur . Il y a une autre façon de faire, mais nous y arriverons bien assez tôt.

  2. La Charge", 1519710920"

    La charge que nous allons utiliser pour cette injection commence avec la séquence d'octets 0xbf27 . Dans gbk , c'est un caractère multibyte invalide; dans latin1 , c'est la chaîne ¿' . Notez que dans latin1 et gbk , 0x27 sur son propre est un littéral ' caractère.

    nous avons choisi cette charge utile parce que, si nous appelions addslashes() dessus, nous insérerions un ASCII \ i.e. 0x5c , avant le caractère ' . Donc, nous finirions avec 0xbf5c27 , qui dans gbk est une séquence de deux caractères: 0xbf5c suivi de 0x27 . Ou en d'autres termes, un valide caractère suivi d'un ' non enregistré . Mais on n'utilise pas addslashes() . Donc à la la prochaine étape...

  3. $stmt - > execute()

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

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

    donc le appel à mysql_real_escape_string() insère le backslash, et nous avons un libre accrochage ' caractère dans notre contenu "échappé"! En fait, si nous devions regarder $var dans le jeu de caractères gbk , nous verrions:

    縗' 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 des déclarations préparées par AOP...

La Seule Solution

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

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

cela donnera habituellement une véritable déclaration préparée (c.-à-d. que les données sont envoyées dans un paquet distinct de la requête). Cependant, soyez conscient que PDO va silencieusement failback pour émuler des déclarations que MySQL ne peut pas préparer nativement: ceux qu'il peut être listés dans le manuel, mais attention à sélectionner la version de serveur appropriée).

La Bonne Solution", 1519820920"

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, nous serions bien fourni nous utilisons une version MySQL depuis 2006.

si vous utilisez une version antérieure de MySQL, alors un bug dans" 151939092020 "signifie que les caractères multi-octets invalides tels que ceux de notre charge utile ont été traités comme des octets simples pour échapper aux fins même si le client avait été correctement informé du codage de connexion et donc cette attaque aurait tout de même réussi. 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 précédentes il ne peut pas prévenir cette attaque pour toutes les commandes possibles! Il est maintenant exposé comme un paramètre DSN , qui devrait être utilisé au lieu de SET NAMES ...

L'Économie Grace

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 non vulnérables et qui, pourtant, peuvent soutenir tous "les 1519950920" caractère Unicode: alors vous pourriez choisir d'utiliser à la place-mais il a seulement été disponible depuis MySQL 5.5.3. Une alternative est utf8 , qui est aussi non vulnérables et peut prendre en charge l'ensemble de l'Unicode Plan Multilingue de Base .

alternativement, vous pouvez activer le mode NO_BACKSLASH_ESCAPES 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 donc le processus d'échappement ne peut pas créer des caractères valides dans les vulnérables codages où ils n'existaient pas auparavant (c'est à dire 0xbf27 est toujours 0xbf27 etc.-le serveur sera toujours rejeter la chaîne comme non valide. 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 De Sécurité

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 déclarations é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 réglé le jeu de caractères correctement.

$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 déclarations préparées tout le temps.

Enveloppant

si vous:

  • utiliser les Versions modernes de MySQL (fin 5.1, tous 5.5, 5.6, etc) et paramètre DSN de PDO (en PHP ≥ 5.3.6)

ou

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

ou

  • Activer NO_BACKSLASH_ESCAPES mode SQL

vous êtes 100% en sécurité.

sinon, vous êtes vulnérable même si vous utilisez des déclarations préparées par AOP...

Addendum

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, C'est que beaucoup de tests se cassent quand je fais ça. Un problème est que emulated prepares ne lancera que des erreurs de syntaxe sur execute, mais true prepares lancera des erreurs sur prepare. Donc, cela peut causer des problèmes (et fait partie de la raison pour laquelle les tests sont borking).

702
répondu ircmaxell 2017-05-23 12:34:48

les déclarations préparées / les requêtes paramétrisées sont généralement suffisantes pour prévenir 1er ordre injection sur cette déclaration * . Si vous utilisez un SQL dynamique non vérifié ailleurs dans votre application, vous êtes toujours vulnérable à l'injection de deuxième ordre .

injection de 2ème ordre signifie que les données ont été parcourues une fois dans la base de données avant d'être incluses dans une requête, et est beaucoup plus difficile à retirer. AFAIK, vous ne voyez presque jamais de vraies attaques d'ordre 2, car il est généralement plus facile pour les attaquants de faire de l'ingénierie sociale, mais vous avez parfois des bogues d'ordre 2 qui apparaissent en raison de caractères extra bénins ' ou similaires.

vous pouvez accomplir une attaque d'injection de 2ème ordre quand vous pouvez faire qu'une valeur soit stockée dans une base de données qui est plus tard utilisée comme un littéral dans une requête. Par exemple, supposons que vous saisissiez les informations suivantes comme nouveau nom d'utilisateur. lors de la création d'un compte sur un site web (en supposant 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 déclaration préparée ferait toujours en sorte que la requête intégrée ci-dessus ne s'exécute pas au moment de l'insertion, et stockerait la valeur correctement dans la base de données. Cependant, imaginez que plus tard l'application récupère votre nom d'utilisateur de la base de données, et utilise la concaténation chaîne de caractères pour inclure cette valeur une nouvelle requête. Vous pourriez obtenir voir quelqu'un d'autre mot de passe. Depuis les premiers noms dans la table des utilisateurs ont tendance à être admins, vous avez également peut-être juste donné la ferme. (Note aussi: c'est une raison de plus pour ne pas stocker les mots de passe en clair!)

nous voyons, alors, que les déclarations préparées sont suffisantes pour une seule requête, mais par eux - mêmes ils sont pas suffisant pour protéger contre les attaques d'injection sql tout au long d'une application, parce qu'ils manquent d'un mécanisme pour faire respecter ce tous les accès à une base de données dans l'application utilise le code sécurisé. Cependant, utilisé dans le cadre de la bonne conception de l'application - qui peut inclure des pratiques telles que l'examen du code ou l'analyse statique, ou l'utilisation d'un ORM, couche de données, ou couche de service qui limite la dynamique sql - "déclarations préparées sont l'outil principal pour résoudre le problème D'Injection Sql. si vous suivez de bons principes de conception de l'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 paramétrage. Dans ce cas, l'injection sql (premier et second ordre) est complètement empêché.


* il s'avère que MySql / PHP sont (ok, étaient) juste stupide sur la manipulation des paramètres quand de larges caractères sont impliqués, et il ya encore un rare cas souligné dans le autre réponse très votée ici qui peut permettre à l'injection de glisser à travers une requête paramétrée.

496
répondu Joel Coehoorn 2017-07-18 16:53:12

Non, ils ne le sont pas toujours.

cela dépend de si vous permettez à l'utilisateur d'entrer 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 de déclarations préparées dans cet exemple ne fonctionnera pas, parce que l'entrée de l'utilisateur est utilisée comme un identificateur, pas comme des données. 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']) );

Note: vous ne pouvez pas utiliser PDO pour lier les données qui sortent de 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 données . AOP ne peut s'échapper que pour data . Deuxièmement, vous ne pouvez même pas mettre ' citations autour de lui. 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 .

40
répondu Tower 2012-09-27 13:20:25

Oui, c'est suffisant. La façon dont les attaques de type injection fonctionnent, c'est en obtenant d'une manière ou d'une autre un interprète (la base de données) pour évaluer quelque chose, qui 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 sur le même support (par ex. lorsque vous construisez une requête comme une chaîne de caractères).

Paramétrer des requêtes en envoyant le code et les données séparément, de sorte qu'il serait jamais être possible de trouver un trou.

vous pouvez néanmoins ê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.

24
répondu troelskn 2008-09-25 15:55:46

Non cela ne suffit pas (dans certains cas spécifiques)! Par défaut, PDO utilise des déclarations préparées émulées en utilisant MySQL comme pilote de base de données. Vous devez toujours désactiver les déclarations émulées en utilisant MySQL et PDO:

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

une autre chose qui doit toujours être fait il définir le codage correct 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 liée: Comment puis-je empêcher L'injection SQL en PHP?

notez Également que c'est seulement sur la base de données côté des choses que vous auriez encore à vous regarder lors de l'affichage des données. Par exemple: en utilisant htmlspecialchars() à nouveau avec le style d'encodage et de citation correct.

24
répondu PeeHaa 2017-05-23 12:34:48

personnellement, je voudrais toujours exécuter une certaine forme d'assainissement sur les données d'abord comme vous ne pouvez jamais faire confiance à l'entrée de l'utilisateur, cependant en utilisant placeholders / parameter binding les données saisies sont envoyées au serveur séparément à la déclaration sql et puis binded ensemble. La clé ici est que cette lie les données fournies à un type spécifique et une utilisation spécifique et élimine toute possibilité de changer la logique de l'instruction SQL.

9
répondu JimmyJ 2008-09-25 15:50:43

Eaven si vous voulez empêcher l'injection sql front-end, en utilisant des vérifications html ou js, vous devez considérer que les vérifications front-end sont "contournables".

vous pouvez désactiver js ou éditer un motif 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 le droit d'assainir entrée Date backend à l'intérieur de votre contrôleur.

je voudrais vous suggérer pour utiliser la fonction PHP native filter_input() afin d'assainir 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 sensées, je voudrais vous suggérer d'utiliser l'expression régulière pour valider le format de données. preg_match() vous aideront dans ce cas! Mais prenez soin de vous! Moteur d'expressions régulières n'est pas si léger. Utilisez - le SEULEMENT si nécessaire, sinon vos performances d'application diminueront.

la sécurité a un coût, mais ne gaspillez pas votre la performance!

Exemple facile:

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

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

Donc, la réponse finale est: "Non! AOP Préparées ne pas empêcher tout type d'injection sql"; Elle n'empêche pas des valeurs inattendues, juste inattendu concaténation

-2
répondu snipershady 2018-03-04 20:17:56