web-dev-qa-db-fra.com

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 de l'AOP indique:

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

Est-ce vraiment tout ce que j'ai à faire pour éviter les injections SQL? Est-ce vraiment aussi simple?

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

622
Mark Biek

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

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

La réponse longue 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 renverra plus d'une ligne. Disséquons 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 attendu par le serveur sur la connexion pour coder _'_ comme dans ASCII ie _0x27_ et pour avoir un caractère dont le dernier octet est un ASCII _\_ c'est-à-dire _0x5c_. Il s’avère que 5 de ces encodages sont pris en charge dans MySQL 5.6 par défaut: _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 que nous allons utiliser pour cette injection commence par la séquence d'octets _0xbf27_. Dans gbk, il s'agit d'un caractère multi-octets non valide; Dans _latin1_, c'est la chaîne _¿'_. Notez que dans _latin1_ et gbk, _0x27_ est seul un caractère littéral _'_ .

    Nous avons choisi cette charge parce que, si nous appelions addslashes(), nous insérerions un ASCII _\_ c'est-à-dire _0x5c_, avant le caractère _'_ . Nous aboutirions donc 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 _'_ non échappé. Mais nous n'utilisons pas addslashes(). Passons à l'étape suivante ...

  3. $ stmt-> execute ()

    La chose importante à réaliser ici est que PDO par défaut ne () _ pas _ ne fait de vraies instructions 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() (fonction API C de MySQL) 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. Ainsi, il peut exécuter correctement l'échappement pour le jeu de caractères attendu par le serveur. Cependant, jusqu'à présent, le client pense que nous utilisons toujours _latin1_ pour la connexion, car nous ne lui avons jamais dit le contraire. Nous avons dit au serveur que nous utilisons gbk, mais le client pense toujours que c'est _latin1_ .

    Par conséquent, l'appel à mysql_real_escape_string() insère la barre oblique inverse et nous avons un caractère suspendu libre _'_ dans notre contenu "échappé"! En fait, si nous examinions _$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 utilisant les instructions préparées PDO ...

La solution simple

Il convient maintenant 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 aboutira généralement à une instruction réellement préparée (c'est-à-dire que les données sont envoyées dans un paquet séparé de la requête). Cependant, sachez que PDO utilisera silencieusement repli pour émuler des instructions que MySQL ne peut pas préparer en mode natif: celles qu’il peut utiliser sont répertoriées dans le manuel, mais méfiez-vous version du serveur).

Le correct correctif

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

Si vous utilisez une version antérieure de MySQL, un bug dans mysql_real_escape_string() signifiait que des caractères multi-octets non valides, tels que ceux de notre charge utile, étaient traités comme des octets simples à des fins d'échappement même si le client avait été correctement informé du codage de la connexion et que cette attaque réussirait toujours. Le bogue a été corrigé dans MySQL 4.1.2 , 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() jusqu’à la version 5.3.6. Ainsi, dans les versions précédentes, il ne le pouvait pas empêcher cette attaque pour chaque commande possible! 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 codée à l'aide d'un jeu de caractères vulnérable. utf8mb4 est non vulnérable et peut néanmoins prendre en charge tous les caractères Unicode: vous pouvez donc choisissez d’utiliser cela à la place, mais il n’est disponible que depuis MySQL 5.5.3. Une alternative est utf8 , qui est également non vulnérable et peut prendre en charge la totalité du Unicode plan multilingue de base =.

Vous pouvez également activer le mode SQL NO_BACKSLASH_ESCAPES , qui (entre autres choses) modifie le fonctionnement de mysql_real_escape_string(). Lorsque ce mode est activé, _0x27_ sera remplacé par _0x2727_ plutôt que _0x5c27_ et le processus d'échappement ne peut créer de caractères valides dans aucun des les encodages vulnérables où ils n'existaient pas auparavant (c'est-à-dire _0xbf27_ est toujours _0xbf27_ etc.) - de sorte que le serveur rejettera toujours la chaîne comme non valide. Cependant, voir réponse de @ eggyal pour obtenir une autre vulnérabilité pouvant découler de l’utilisation de ce mode SQL (bien que ce ne soit pas le cas avec PDO).

Exemples sécuritaires

Les exemples suivants sont sans danger:

_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 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 défini 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 instructions préparées tout le temps.

Emballer

Si vous:

  • Utilisez les versions modernes de MySQL (version 5.1, versions 5.5, 5.6, etc.) ET paramètre de jeu de caractères DSN du PDO (dans PHP ≥ 5.3.6)

OU

  • N'utilisez pas de jeu de caractères vulnérable pour le codage de la connexion (vous utilisez uniquement _utf8_/_latin1_/ascii/etc)

OU

  • Activer le mode _NO_BACKSLASH_ESCAPES_ SQL

Vous êtes 100% en sécurité.

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

Addenda

Je travaille lentement sur un correctif pour changer la valeur par défaut afin de ne pas émuler, cela prépare une future version de PHP. Le problème que je rencontre est que BEAUCOUP de tests sont rompus lorsque je le fais. Un des problèmes est que les préparations émulées ne génèrent que des erreurs de syntaxe lors de l'exécution, mais true, prépare des erreurs lors de la préparation. Cela peut donc poser problème (et cela fait partie des raisons pour lesquelles les tests échouent).

764
ircmaxell

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

L'injection de second ordre signifie que les données ont été cyclées dans la base de données une fois avant d'être incluses dans une requête et sont beaucoup plus difficiles à extraire. Si je comprends bien, vous ne verrez presque jamais de véritables attaques d'ingénierie de second ordre, car il est généralement plus facile pour les attaquants de se familiariser avec l'ingénierie sociale, mais il arrive parfois que des bogues de second ordre apparaissent à cause de personnages ' extra bénins ou similaires.

Vous pouvez effectuer une attaque par injection de second ordre lorsque vous pouvez faire en sorte qu'une valeur soit stockée dans une base de données utilisée ultérieurement en tant que littéral dans une requête. Par exemple, supposons que vous entrez les informations suivantes comme nouveau nom d'utilisateur lors de la création d'un compte sur un site Web (en supposant que la base de données MySQL est utilisée 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 s'assurerait toujours que la requête incorporé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 l'application récupère ultérieurement votre nom d'utilisateur dans la base de données et utilise la concaténation de chaînes pour inclure cette valeur dans une nouvelle requête. Vous pourriez peut-être voir le mot de passe de quelqu'un d'autre. Comme les premiers noms de la table users ont tendance à être des administrateurs, vous venez peut-être de céder la ferme. (Remarque: ceci est une raison de plus de ne pas stocker les mots de passe en texte brut!)

Nous voyons donc que les instructions préparées sont suffisantes pour une requête unique, mais qu’elles ne sont pas non suffisantes pour se protéger contre les attaques par injection SQL dans l’ensemble d’une application. , car ils ne disposent pas d’un mécanisme permettant de faire en sorte que tous les accès à une base de données au sein de l’application utilisent un code sécurisé. Cependant, utilisé dans le cadre d’une bonne conception d’applications - pouvant 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 SQL dynamique - Les instructions préparées sont le principal outil permettant de résoudre le problème de l'injection SQL. Si vous suivez de bons principes de conception d'applications, tels que votre accès aux données est séparé du reste de votre programme, il devient facile d'imposer ou de vérifier que chaque requête utilise correctement le paramétrage. Dans ce cas, l'injection de SQL (premier et deuxième ordre) est complètement empêchée.


*Il s'avère que MySql/PHP sont (d'accord, étaient) juste stupides à propos de la gestion des paramètres lorsque des caractères larges sont impliqués, et qu'il y a toujours un cas rare décrit dans le autre réponse très votée ici qui peut permettre à l’injection de glisser à travers une requête paramétrée.

507
Joel Coehoorn

Non, ils ne sont pas toujours.

Cela dépend si vous autorisez la saisie de l'utilisateur 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 fonctionnera pas, car l'entrée utilisateur est utilisée comme identifiant et non comme 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 en dehors de DDL (langage de définition de données), 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, c'est parce que DESC et ASC ne sont pas données. PDO ne peut s'échapper que pendant data. Deuxièmement, vous ne pouvez même pas mettre ' entre guillemets. Le seul moyen d'autoriser le tri choisi par l'utilisateur est de filtrer manuellement et de vérifier qu'il s'agit bien de DESC ou ASC.

40
Tower

Oui, c'est suffisant. La manière dont les attaques par type d'injection fonctionnent consiste à demander à un interprète (la base de données) d'évaluer quelque chose, ce qui aurait dû être des données, comme s'il s'agissait de code. Cela n’est possible que si vous mélangez du code et des données sur 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 serait jamais possible de trouver un trou dans celle-ci.

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

26
troelskn

Non, cela ne suffit pas (dans certains cas)! Par défaut, PDO utilise des instructions préparées émulées lorsque MySQL est utilisé comme pilote de base de données. Vous devez toujours désactiver les instructions préparées émulées lorsque vous utilisez MySQL et PDO:

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

Une autre chose qui devrait toujours être faite est de définir 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 également cette question connexe: Comment puis-je empêcher l’injection SQL en PHP?

Notez également que cela ne concerne que le côté base de données que vous auriez toujours à surveiller vous-même lors de l'affichage des données. Par exemple. en utilisant à nouveau htmlspecialchars() avec le style de codage et de citation correct.

25
PeeHaa

Personnellement, je voudrais toujours exécuter une forme d'assainissement sur les données d'abord car vous ne pouvez jamais faire confiance à une entrée utilisateur. Cependant, lorsque vous utilisez une liaison entre paramètres/espaces réservés, les données entrées sont envoyées séparément au serveur avec l'instruction SQL, puis liées entre elles. 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.

10
JimmyJ