[PHP] Les requêtes préparées
par c2c le 17 mar 2009
Une des failles les plus connues, les plus utilisées et les plus simples à utiliser quand on parle de sites Web, c’est évidemment les injections SQL. Même Kaskpersky, le grand nom de la sécurité informatique y passe.
Les requêtes préparées sont, de loin, la méthode la plus fiable et la plus performante pour se protéger.
Je vais essayer dans cet article de vous expliquer pourquoi et surtout comment les utiliser avec PDO (qui rappelons le va devenir le SGBD par défaut de PHP6, les fonctions mysql_*, désuettes, seront recalées au fin fond d’une extension PECL).
Une injection SQL, comment ça marche ?
Le fonctionnement des applications PHP / MySQL (ou autres) depuis des lustres est basé sur un principe simple : L’utilisateur envoi une requête, cette requête est interprétée, une requête SQL en découle et le résultat est mis en forme ou enregistré en base de données.
Pour ce faire, le développeur génère des requêtes SQL en utilisant les données envoyées par l’internaute comme paramètres, par exemple :
mysql_query('UPDATE `users` SET `name`="' . $_POST['name'] . '" WHERE `id_user`=' . $user_id);
Problème : L’internaute envoie ce qu’il veut, et la requête effectuée sur la base de donnée peut être dangereuse. Par exemple, dans l’exemple précédent, si l’internaute envoie
« ; DELETE from users; –
vous allez envoyer comme requête à votre serveur SQL :
UPDATE `users` SET `name`=""; DELETE from users; --" WHERE `id_user`= 5
Soit : Efface tous les noms de ma table puis vide là. Problématique …
L’échappement comme solution miracle …
Face à ce problème évident de sécurité, les développeurs ont souvent le reflexe d’utiliser mysql_escape_string(), ou mieux, mysql_real_escape_string(). Ces deux fonctions de PHP ont pour but de protéger les champs envoyés par les internautes contre ces injections SQL en échappant (en ajoutant un backslash ou ) devant les caractères NULL, x00 (null), n (saut de ligne), r (retour chariot), , ‘, « et x1a (control).
Pour l’exemple précédent, cela donnera :
mysql_query('UPDATE `users` SET `name`="' . mysql_real_escape_string($_POST['name']) . '" WHERE `id_user`=' . mysql_real_escape_string($user_id));
soit la requête :
UPDATE `users` SET `name`=""; DELETE from users; --" WHERE `id_user`= 5
Youpi nous voila protégé ! L’article est fini ? Non.
… ou pas !
mysql_real_escape_string() n’est pas totalement sécurisé
Et oui, on nous ment, on nous spolie ! Bon, la on passe au niveau barbu du hacking (les enfants, passez au point suivant, ne refaites pas cela chez vous).
mysql_real_escape_string() se base sur le jeu de caractère de la connexion active (SET NAMES) et si celui-ci n’est pas actif, elle utilise le jeu de caractère par défaut. Il est alors facile d’utiliser la magie de « chr(0xbf) . chr(0x27)" pour insérer l'injection fatale.Cependant, cela ne marche que pour des jeu de caractères comme GBK ou Big5, donc il est probable qu’un SET NAMES soit présent.
Vous n’avez pas compris ? Pas grave, retenez juste que malgré ce que dit le complot universel, mysql_real_escape_string() n’est pas infaillible !
Alors pourquoi ne pas encoder la chaîne ?
Effectivement, si l’on utilise htmlentities() qui convertit chaque caractère qui peut l’être en entité HTML (&quelquechose), ou base64_encode(), etc. il semble que l’on sera protégés ?
Oui et non, déjà non, on ne sera pas COMPLÈTEMENT protégé, pour les même raisons que le point précédent. Et puis vous allez vous compliquer la vie lorsque vous souhaiterez rechercher du texte (oui, rechercher des entités HTML c’ets pas sexy).
Et puis surtout, ce n’est pas optimisé !
Un des goulot d’étranglement d’une application web ou d’un site, c’est souvent la base de donnée ; alors en plus y rajouter des traitement, recherche et modifications permanents de chaîne, c’est du suicide et du massacre de cycle de processeur !
Puis vinrent les requêtes préparées !
Les requêtes préparées, ou prepared queries, c’est la bonne réponse, pour la simple et bonne raison : Ce n’est pas un patch mais une refonte de la reflexion !
Posons nous une seconde sur le problème, la source de la faille repose sur le fait qu’un serveur SQL quel qu’il soit se base sur un concept : PHP se connecte, lui donne une requête (un ordre) qu’il execute sans broncher.Le script lui prend une donnée qui n’est pas contrôlée par le développeur et la place au milieu de l’ordre donnée.
Il n’y a donc qu’une solution : Séparer ce que l’on contrôle, l’ordre, de ce que l’on ne controle pas, les arguments.
Le principe de la requête préparée, c’est ça. On dit au serveur SQL :
Tu vas modifier le champs name par une certaine valeur la table users quand le champs id sera d’un autre certaine valeur
Puis, on lui précise
La première valeur c’est $_POST['name'], et la seconde c’est 5
La première partie fera l’objet d’un analyse du serveur SQL, la seconde ne le sera pas. Voila, on est protégé, on est optmisé la messe est dite
Donc, comment ça marche ?
Qui dit requête préparée en PHP, dit PDO (ou Zend Framework qui vous simplifie la vie, voila il est placé).
Plutôt qu’un long discours, voici un exemple qui insere et lit des données, pour aller plus loin, je vous laisse lire la doc.
/**
* Création d'un objet PDO
*/
$object = new PDO('mysql:host=localhost;dbname=myDatabase', 'user', 'pass');
/**
* On énonce une requête préparée
*/
$statement = $object->prepare("SELECT * FROM `user` WHERE `name`=? OR `firstname`=?");
/**
* On éxécute l'énoncé avec une paire d'argument
*/
$statement->execute(array($_POST['name'], $_POST['firstname']));
/**
* On récupère le résultat
*/
$result = $statement->fetchRow();
/**
* On énonce une requête préparée d'insertion
*/
$statement = $object->prepare("INSERT INTO `users` (`name`, `firstname`, `email`) VALUES (?, ?, ?)");
/**
* On éxécute l'énoncé pour 4 valeurs différentes
*/
$statement->execute(array('doe', 'john', 'john.doe@gmail.com'));
$statement->execute(array('gates', 'bill', 'bill@microsoft.com'));
$statement->execute(array('jobs', 'steve', 'sjobs@apple.com'));
$statement->execute(array('"; DELETE from users; --', 'blah', 'bleh'));
D’autres intérêts ?
Utiliser des requêtes préparées en PDO ne vous apportera pas que la sécurisation de vos requêtes.
Déjà, utiliser PDO vous apportera une vraie conception objet de la gestion de la base de données (mais encore plus avec Zend Framework
) mais j’y reviendrais avec un billet sur MVC.
Mais en plus, si vous regardez bien l’exemple, j’ai fait 4 requêtes avec une énoncé, donc concretement, votre serveur SQL n’a analysé qu’une fois votre requête et vous n’avez fait aucune analyse des paramètres.
Je vous laisse méditer sur l’optimisation obtenue

17 mars 2009 à 12:00:56
Très bon article!
Les requêtes préparées sous PDO permettent à MySQL de se rapprocher des fonctionnalités d’Oracle, qui implémente depuis un bon moment déjà les « bind variables » dont le principe est exactement le même.
Merci pour ce point que tous les développeurs devraient lire !
17 mars 2009 à 12:01:15
Très bon article. J’adore les quelques phrases qui accompagnent mysql_string_real_escape.
Je vais me pencher sur ça très rapidement je pense.
Merci !!
17 mars 2009 à 12:58:33
Merci ! Ca fait plaisir vos commentaires !
17 mars 2009 à 13:27:41
Encore un très bon article sur des points pas forcément abordés ailleurs.
Merci
20 mars 2009 à 12:11:34
Excellent et tres instructif.
20 mars 2009 à 12:14:31
ooops, j allais ajouter :
QUID des prepared queries dans les frameworks php style Cakephp ou Code Igniter ?
20 mars 2009 à 12:27:52
CakePHP : Non. C’est LE POINT problématique de CakePHP selon moi d’ailleurs … bon point à soulever.
Tout est dit ici : http://api.cakephp.org/view_source/dbo-mysql/#line-410
Comment on peut se prétendre « The best PHP Framework » et encore utiliser mysql_query ??
Pour Code Igniter je ne sais pas…
24 mars 2009 à 09:25:24
Pour CI, il me semble que le problème est le même. Ce sont deux frameworks très similaires même si Cakephp est plus aboutit.
Du coup je me demande si le développement avec Cakephp est une solution pérenne ?
Le gros soucis avec Zend, c’est sa prise en main quand on est pas développeur avancé. C’est bien documenté et c’est certain que développer avec Zend Framework présente un gros avantage pour coller avec les dernières fonctionnalités de PHP, mais vraiment, moi j y arrive pas
27 mars 2009 à 11:48:51
@Daibai :
Le Zend framework compte de nombreux adeptes a travers le monde, tu trouvera donc tres facilement des tutoriaux pour tout niveaux.
Etant developpé par une partie des developpeurs de PHP, il est donc tres au fait des nouveautes des futures versions de PHP et donc ses mises a jour sont frequentes.
Et pour completer, Zend framework compte desormais deux livres en francais pour apprendre a l’utiliser. Je conseillerai l’excellent livre sur le sujet de julien pauli, l’autre etant, je trouve mais ce n’est qu’un avis personnel, trop brouillon, donc pas evident pour commencer.
Sinon pour parler des frameworks, il y a egalement symfony, mais il se base sur du Zend framework, donc autant apprendre la base directement
27 mars 2009 à 14:44:35
» Comment on peut se prétendre “The best PHP Framework” et encore utiliser mysql_query ?? »
Parceque le Zend Framework via PDO n’utilisepas mysql_query ?
Je suis curieux de savoir comment c’est possible.
La requête mysql_query n’est pas utilisée de facon visible peut être, mais si on regarde dans le code source (classes)
Ou alors j’ai fait une découverte en lisant ce post, non ?
27 mars 2009 à 14:50:48
Passant qui passe : Tu peux fouiller encore et encore les sources de ZF, tu ne trouvera jamais un seul mysql_query !
27/03 14:48 c2c@box ~/library/Zend% grep -Ri ‘mysql_query’ *
27/03 14:48 c2c@box ~/library/Zend%
Dieu merci !
Tu devrais te pencher sur PDO (http://fr.php.net/pdo), et vite, car à partir de PHP6, l’extension mysql ne sera plus qu’un package PECL (déprécié) et remplacé par PDO_mysql
27 mars 2009 à 15:04:59
Salut c3c,
merci pour ta réponse, je vais me pencher la dessus très bientôt.
Juste une question, est il possible de se connecter a la BDD via PDO et utiliser les anciennes requêtes ensuite ?
En d’autres termes est-il possible d’upgrader progressivement un site ou faut-il être entièrement passé à PDO pour que cela fonctionne ?
Merci encore pour l’info.
A vrai dire PDO semble en effet bien plus ergonomique, rapide et efficace.
Je vais passer à ZF dasn les jours qui viennent de toute façon ^^
Bon après midi
27 mars 2009 à 15:22:57
Il n’est pas possible d’utiliser les fonction mysql_ ou mysqli avec un objet PDO.
Enfin, tu peux utiliser les deux en même temps mais pas les mixer quoi …
Par contre tu peux utiliser PDO ou Zend_DB avec des requêtes « inline » et pas des requêtes préparées.
11 juillet 2009 à 03:46:57
salut;
je voulais juste signaler que dans l’exemple de l’injection SQL filtrée par mysql_real_escape_string vous avez oublié le filtrage justement , la requete est identique a celle non filtrée.
Sinon au sujet de :
« mysql_real_escape_string() se base sur le jeu de caractère de la connexion active (SET NAMES) »
On parle de l’encodage du navigateur là ?
Peut etre auriez vous un lien ou 2 developpant ce probleme svp?