PDO MySQL: Utilisez PDO :: ATTR_EMULATE_PREPARES ou pas?

117

Voici ce que j'ai lu jusqu'à présent PDO::ATTR_EMULATE_PREPARES:

  1. L'émulation de préparation de PDO est meilleure pour les performances puisque la préparation native de MySQL contourne le cache de requête .
  2. La préparation native de MySQL est meilleure pour la sécurité (empêchant l'injection SQL) .
  3. La préparation native de MySQL est meilleure pour les rapports d'erreurs .

Je ne sais plus à quel point ces déclarations sont vraies. Ma plus grande préoccupation en choisissant une interface MySQL est d'empêcher l'injection SQL. La deuxième préoccupation est la performance.

Mon application utilise actuellement MySQLi procédural (sans instructions préparées) et utilise assez souvent le cache de requêtes. Il réutilisera rarement les instructions préparées dans une seule requête. J'ai commencé le passage à PDO pour les paramètres nommés et la sécurité des déclarations préparées.

J'utilise MySQL 5.1.61etPHP 5.3.2

Dois-je laisser PDO::ATTR_EMULATE_PREPARESactivé ou non? Existe-t-il un moyen d'avoir à la fois les performances du cache de requêtes et la sécurité des instructions préparées?

Andrew Ensley
la source
3
Honnêtement? Continuez simplement à utiliser MySQLi. Si cela fonctionne déjà en utilisant des instructions préparées sous cela, PDO est fondamentalement une couche d'abstraction inutile. EDIT : PDO est vraiment utile pour les applications de champ vert où vous ne savez pas quelle base de données va dans le back-end.
jmkeyes
1
Désolé, ma question n'était pas claire auparavant. Je l'ai édité. L'application n'utilise pas d'instructions préparées dans MySQLi pour le moment; juste mysqli_run_query (). D'après ce que j'ai lu, les instructions préparées par MySQLi contournent également le cache de requêtes.
Andrew Ensley

Réponses:

108

Pour répondre à vos préoccupations:

  1. MySQL> = 5.1.17 (ou> = 5.1.21 pour les instructions PREPAREet EXECUTE) peut utiliser des instructions préparées dans le cache de requêtes . Ainsi, votre version de MySQL + PHP peut utiliser des instructions préparées avec le cache de requêtes. Cependant, notez attentivement les mises en garde concernant la mise en cache des résultats des requêtes dans la documentation MySQL. Il existe de nombreux types de requêtes qui ne peuvent pas être mises en cache ou qui sont inutiles même si elles sont mises en cache. D'après mon expérience, le cache de requêtes n'est pas souvent une très grande victoire de toute façon. Les requêtes et les schémas nécessitent une construction spéciale pour utiliser au maximum le cache. Souvent, la mise en cache au niveau de l'application finit par être nécessaire de toute façon à long terme.

  2. Les préparations natives ne font aucune différence pour la sécurité. Les instructions pseudo-préparées échapperont toujours aux valeurs des paramètres de requête, cela sera juste fait dans la bibliothèque PDO avec des chaînes au lieu de sur le serveur MySQL en utilisant le protocole binaire. En d'autres termes, le même code PDO sera également vulnérable (ou non vulnérable) aux attaques par injection quel que soit votre EMULATE_PREPARESréglage. La seule différence est l'endroit où le remplacement des paramètres se produit - avec EMULATE_PREPARES, il se produit dans la bibliothèque PDO; sans EMULATE_PREPARES, il se produit sur le serveur MySQL.

  3. Sans EMULATE_PREPARESvous pouvez obtenir des erreurs de syntaxe au moment de la préparation plutôt qu'au moment de l'exécution; avec EMULATE_PREPARESvous n'obtiendrez des erreurs de syntaxe qu'au moment de l'exécution car PDO n'a pas de requête à donner à MySQL jusqu'au moment de l'exécution. Notez que cela affecte le code que vous allez écrire ! Surtout si vous utilisez PDO::ERRMODE_EXCEPTION!

Une considération supplémentaire:

  • Il y a un coût fixe pour a prepare()(en utilisant des instructions préparées natives), donc un prepare();execute()avec des instructions préparées natives peut être un peu plus lent que d'émettre une requête textuelle en utilisant des instructions préparées émulées. Sur de nombreux systèmes de base de données, le plan de requête pour a prepare()est également mis en cache et peut être partagé avec plusieurs connexions, mais je ne pense pas que MySQL le fasse. Donc, si vous ne réutilisez pas votre objet instruction préparé pour plusieurs requêtes, votre exécution globale peut être plus lente.

Comme recommandation finale , je pense qu'avec les anciennes versions de MySQL + PHP, vous devriez émuler des déclarations préparées, mais avec vos versions très récentes, vous devriez désactiver l'émulation.

Après avoir écrit quelques applications qui utilisent PDO, j'ai créé une fonction de connexion PDO qui a ce que je pense être les meilleurs paramètres. Vous devriez probablement utiliser quelque chose comme ça ou modifier vos paramètres préférés:

/**
 * Return PDO handle for a MySQL connection using supplied settings
 *
 * Tries to do the right thing with different php and mysql versions.
 *
 * @param array $settings with keys: host, port, unix_socket, dbname, charset, user, pass. Some may be omitted or NULL.
 * @return PDO
 * @author Francis Avila
 */
function connect_PDO($settings)
{
    $emulate_prepares_below_version = '5.1.17';

    $dsndefaults = array_fill_keys(array('host', 'port', 'unix_socket', 'dbname', 'charset'), null);
    $dsnarr = array_intersect_key($settings, $dsndefaults);
    $dsnarr += $dsndefaults;

    // connection options I like
    $options = array(
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
    );

    // connection charset handling for old php versions
    if ($dsnarr['charset'] and version_compare(PHP_VERSION, '5.3.6', '<')) {
        $options[PDO::MYSQL_ATTR_INIT_COMMAND] = 'SET NAMES '.$dsnarr['charset'];
    }
    $dsnpairs = array();
    foreach ($dsnarr as $k => $v) {
        if ($v===null) continue;
        $dsnpairs[] = "{$k}={$v}";
    }

    $dsn = 'mysql:'.implode(';', $dsnpairs);
    $dbh = new PDO($dsn, $settings['user'], $settings['pass'], $options);

    // Set prepared statement emulation depending on server version
    $serverversion = $dbh->getAttribute(PDO::ATTR_SERVER_VERSION);
    $emulate_prepares = (version_compare($serverversion, $emulate_prepares_below_version, '<'));
    $dbh->setAttribute(PDO::ATTR_EMULATE_PREPARES, $emulate_prepares);

    return $dbh;
}
Francis Avila
la source
26
Re # 2: valeurs sûrement que MySQL reçoit en tant que paramètres (aux états préparés natifs) ne sont pas analysées pour SQL du tout ? Le risque d'injection doit donc être inférieur à celui de l'utilisation de l'émulation de préparation de PDO, où toute faille dans l'échappement (par exemple les problèmes historiques mysql_real_escape_stringrencontrés avec les caractères multi-octets) laisserait toujours une vulnérabilité aux attaques par injection?
eggyal
2
@eggyal, vous faites des hypothèses sur la manière dont les instructions préparées sont implémentées. PDO peut avoir un bogue dans ses préparations émulées, mais MySQL peut aussi avoir des bogues. AFAIK, aucun problème n'a été découvert avec les préparations émulées qui pourraient faire passer les littéraux de paramètres sans échappement.
Francis Avila
2
Réponse géniale, mais j'ai une question: si vous désactivez EMULATION, l'exécution ne sera-t-elle pas plus lente? PHP devrait envoyer la déclaration préparée à MySQL pour validation et ensuite seulement envoyer les paramètres. Donc, si vous utilisez l'instruction préparée 5 fois, PHP parlera à MySQL 6 fois (au lieu de 5). Cela ne va-t-il pas ralentir? De plus, je pense qu'il y a plus de chances que PDO ait des bogues dans le processus de validation, plutôt que MySQL ...
Radu Murzea
6
Notez les points soulevés dans cette réponse concernant l'émulation d'instructions préparées en utilisant mysql_real_escape_stringsous le capot et les vulnérabilités qui peuvent survenir (dans des cas extrêmes très particuliers).
eggyal
6
+1 Bonne réponse! Mais pour mémoire, si vous utilisez la préparation native, les paramètres ne sont jamais échappés ou combinés dans la requête SQL, même du côté du serveur MySQL. Au moment où vous exécutez et fournissez des paramètres, la requête a été analysée et transformée en structures de données internes dans MySQL. Lisez ce blog d'un ingénieur d'optimisation MySQL qui explique ce processus: guilhembichot.blogspot.com/2014/05/ ... Je ne dis pas que cela signifie que la préparation native est meilleure, dans la mesure où nous faisons confiance au code PDO pour s'échapper correctement (ce que je faire).
Bill Karwin le
9

Méfiez-vous de la désactivation PDO::ATTR_EMULATE_PREPARES(l'activation du natif prépare) lorsque votre PHP pdo_mysqln'est pas compilé mysqlnd.

Parce que old libmysqln'est pas entièrement compatible avec certaines fonctions, cela peut conduire à d'étranges bogues, par exemple:

  1. Perte de bits les plus significatifs pour les entiers 64 bits lors de la liaison en tant que PDO::PARAM_INT(0x12345678AB sera rogné à 0x345678AB sur une machine 64 bits)
  2. Incapacité de faire des requêtes simples comme LOCK TABLES(cela lève une SQLSTATE[HY000]: General error: 2030 This command is not supported in the prepared statement protocol yetexception)
  3. Besoin de récupérer toutes les lignes du résultat ou de fermer le curseur avant la prochaine requête (avec mysqlndou émulé prépare, il fait automatiquement ce travail pour vous et ne se désynchronise pas avec le serveur mysql)

Ces bogues, j'ai compris dans mon projet simple lors de la migration vers un autre serveur utilisé libmysqlpour le pdo_mysqlmodule. Peut-être qu'il y a beaucoup plus de bugs, je ne sais pas. J'ai également testé sur de nouveaux debian jessie 64 bits, tous les bogues répertoriés se produisent lorsque je apt-get install php5-mysqlet disparaissent lorsque je apt-get install php5-mysqlnd.

Quand PDO::ATTR_EMULATE_PREPARESest défini sur true (par défaut) - ces bogues ne se produisent de toute façon pas, car PDO n'utilise pas du tout les instructions préparées dans ce mode. Donc, si vous utilisez pdo_mysqlbased on libmysql(la sous-chaîne "mysqlnd" n'apparaît pas dans le champ "Client API version" de la pdo_mysqlsection de phpinfo) - vous ne devez pas PDO::ATTR_EMULATE_PREPARESdésactiver.

Pointeur de sauge
la source
3
cette préoccupation est-elle toujours valable en 2019?!
oldboy
8

Je désactiverais les préparations d'émulation lorsque vous exécutez 5.1, ce qui signifie que PDO profitera de la fonctionnalité de déclaration préparée native.

PDO_MYSQL profitera de la prise en charge des instructions préparées natives présentes dans MySQL 4.1 et supérieur. Si vous utilisez une ancienne version des bibliothèques client mysql, PDO les émulera pour vous.

http://php.net/manual/en/ref.pdo-mysql.php

J'ai abandonné MySQLi pour PDO pour les instructions nommées préparées et la meilleure API.

Cependant, pour être équilibré, PDO fonctionne beaucoup plus lentement que MySQLi, mais c'est quelque chose à garder à l'esprit. Je le savais quand j'ai fait le choix et j'ai décidé qu'une meilleure API et l'utilisation de la norme de l'industrie étaient plus importantes que d'utiliser une bibliothèque négligeable plus rapide qui vous relie à un moteur particulier. FWIW Je pense que l'équipe PHP regarde également favorablement PDO par rapport à MySQLi pour l'avenir aussi.

Will Morgan
la source
Merci pour cette information. Comment le fait de ne pas pouvoir utiliser le cache de requêtes a-t-il affecté vos performances ou l'utilisiez-vous même auparavant?
Andrew Ensley
Je ne peux pas dire que j'utilise de toute façon des caches à plusieurs niveaux. Cependant, vous pouvez toujours utiliser explicitement SELECT SQL_CACHE <reste de l'instruction>.
Will Morgan
Je ne savais même pas qu'il y avait une option SELECT SQL_CACHE. Cependant, il semble que cela ne fonctionnerait toujours pas. À partir de la documentation: "Le résultat de la requête est mis en cache s'il peut être mis en cache ..." dev.mysql.com/doc/refman/5.1/en/query-cache-in-select.html
Andrew Ensley
Oui. Cela dépend de la nature de la requête, plutôt que des spécificités de la plate-forme.
Will Morgan
J'ai lu cela comme signifiant "Le résultat de la requête est mis en cache à moins que quelque chose d'autre ne l'empêche d'être mis en cache ", ce qui - d'après ce que j'avais lu jusque-là - comprenait des instructions préparées. Cependant, grâce à la réponse de Francis Avila, je sais que ce n'est plus vrai pour ma version de MySQL.
Andrew Ensley
6

Je recommanderais d'activer de vrais PREPAREappels de base de données car l'émulation ne saisit pas tout .., par exemple, elle se préparera INSERT;!

var_dump($dbh->prepare('INSERT;'));
$dbh->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
var_dump($dbh->prepare('INSERT;'));

Le résultat

object(PDOStatement)#2 (1) {
  ["queryString"]=>
  string(7) "INSERT;"
}
bool(false)

Je prendrai volontiers un coup de performance pour le code qui fonctionne réellement.

FWIW

Version PHP: PHP 5.4.9-4ubuntu2.4 (cli)

Version de MySQL: 5.5.34-0ubuntu0

quickshiftin
la source
C'est un point intéressant. Je suppose que l'émulation reporte l'analyse côté serveur à la phase d'exécution. Bien que ce ne soit pas un gros problème (un SQL erroné finira par échouer), il est plus propre de laisser preparefaire le travail qu'il est censé faire. (De plus, j'ai toujours supposé que l'analyseur de paramètres côté client aura nécessairement ses propres bogues.)
Álvaro González
1
IDK si vous êtes intéressé, mais voici un petit article sur un autre comportement faux que j'ai remarqué avec PDO qui m'a conduit dans ce terrier pour commencer. Il semble que le traitement de plusieurs requêtes manque.
quickshift du
Je viens de regarder quelques bibliothèques de migrations sur GitHub ... Que savez-vous, celle-ci fait à peu près la même chose que mon article de blog.
quickshift du
6

Je suis surpris que personne n'ait mentionné l'une des principales raisons de désactiver l'émulation. Avec l'émulation activée, PDO renvoie tous les nombres entiers et flottants sous forme de chaînes . Lorsque vous désactivez l'émulation, les entiers et les flottants dans MySQL deviennent des entiers et des flottants en PHP.

Pour plus d'informations, consultez la réponse acceptée à cette question: PHP + PDO + MySQL: comment renvoyer des colonnes entières et numériques de MySQL sous forme d'entiers et de nombres en PHP? .

Dallas
la source
5

Pourquoi passer l'émulation à «faux»?

La raison principale en est que le fait que le moteur de base de données effectue la préparation au lieu de PDO est que la requête et les données réelles sont envoyées séparément, ce qui augmente la sécurité. Cela signifie que lorsque les paramètres sont passés à la requête, les tentatives pour y injecter du SQL sont bloquées, car les instructions préparées par MySQL sont limitées à une seule requête. Cela signifie qu'une véritable instruction préparée échouait lorsqu'elle passait une deuxième requête dans un paramètre.

Le principal argument contre l'utilisation du moteur de base de données pour la préparation vs PDO est les deux voyages vers le serveur - un pour la préparation et un autre pour que les paramètres soient transmis - mais je pense que la sécurité supplémentaire en vaut la peine. De plus, au moins dans le cas de MySQL, la mise en cache des requêtes n'a pas été un problème depuis la version 5.1.

https://tech.michaelseiler.net/2016/07/04/dont-emulate-prepared-statements-pdo-mysql/

Harry Bosh
la source
1
La mise en cache des requêtes a quand même disparu : le cache des requêtes est obsolète à partir de MySQL 5.7.20 et est supprimé dans MySQL 8.0.
Álvaro González
0

Pour la petite histoire

PDO :: ATTR_EMULATE_PREPARES = vrai

Cela pourrait générer un effet secondaire désagréable. Il pourrait renvoyer des valeurs int sous forme de chaîne.

PHP 7.4, pdo avec mysqlnd.

Exécution d'une requête avec PDO :: ATTR_EMULATE_PREPARES = true

Colonne: id
Type: entier
Valeur: 1

Exécution d'une requête avec PDO :: ATTR_EMULATE_PREPARES = false

Colonne: id
Type: chaîne
Valeur: "1"

Dans tous les cas, les valeurs décimales sont toujours renvoyées sous forme de chaîne, quelle que soit la configuration :-(

magallanes
la source
les valeurs décimales sont toujours renvoyées une chaîne est le seul moyen correct
Votre bon sens
Oui du point de vue de MySQL mais c'est faux du côté PHP. Java et C # considèrent Decimal comme une valeur numérique.
magallanes le
Non, ce n'est pas le cas. Tout cela est correct pour toute l'informatique. Si vous pensez que c'est faux, alors vous avez besoin d'un autre type, de précision arbitraire
Votre bon sens