Partie 3 : Manipuler une base de données en PHP : sécurité des accès et des données



Manipuler une base de données avec PDO

PDO : Qu'est ce que c'est ?

PDO (Php Data Object) est une interface d'accès à des bases de données en PHP. Elle est inclue dans PHP depuis PHP5 et est l'interface par défaut de connexion aux bases de données depuis PHP6. Elle propose des fonctions permettant de se connecter et d'interroger des bases de données. Ces fonctions d'accès sont universelles : quelque que soit le SGBD utilisé (MySQL, Oracle..), les fonctions utilisées sont les mêmes. C'est un des avantages importants !

Activer PDO

Si vous êtes sous PHP5, PDO n'est peut-être pas activé par défaut. Pour le vérifier, il faut ouvrir le fichier php.ini et chercher les lignes contenant pdo et/ou mysql dans notre cas et s'assurer qu'elles ne soient pas commentées.

À partir de PHP6, nul besoin de s'en préoccuper. PDO est donc l'interface par défaut.

Se connecter à la base avec PDO

PDO est une classe : pour l'utiliser, il faudra donc faire appel à quelques souvenirs de programmation orienté objet. Rappelez vous, en POO, une classe est un moule (comme un moule à gateaux). Elle permet de créer des objets (les gateaux !) en définissant tous leurs attributs et leurs méthodes. Les attributs d'un objet sont des variables propres à l'objet. Par exemple, pour un objet Chat, on pourrait avoir les attributs nom, age, couleur. Les méthodes d'un objet sont des fonctions propres à l'objet. Pour notre objet Chat, on pourrait définir les méthodes manger() et dormir() (oui quoi d'autre ?).

La classe PDO fournie dans PHP va donc nous permettre de créer des objets PDO. Les attributs de l'objet seront entre autre nos paramètres de connexion !

Concrètement, voilà le code à utiliser pour initialiser une connexion à une base de données avec PDO :


try {
	$pdo = new PDO('mysql:host=localhost;dbname=DBNAME', 'root', '');
	$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
}
catch(Exception $e) {
	$msg = 'Erreur ' . $e->getFile() . ' ligne ' . $e->getLine() . ' : ' . $e->getMessage();
	die($msg);
}	
					

Sur la deuxième ligne, vous voyez la création de l'objet PDO $pdo. Le premier paramètre est la chaîne de connexion qui contient le type de connecteur (mysql ici), l'hôte (localhost) et la base de données (DBNAME). Le deuxième paramètre est le login de connexion à la base de données, root par défaut avec WAMP. Le troisième paramètre est le mot de passe, vide par défaut avec WAMP.

Notez bien la troisième ligne : par défaut, PDO n'affiche pas les erreurs qui pourraient se produire lors de l'exécution de vos requêtes. Pas très pratique quand on apprend ou en phase de développement ! On modifie donc l'attribut PDO::ATTR_ERRMODE avec la valeur PDO::ERRMODE_EXCEPTION. Les erreurs seront ainsi traitées comme des exceptions, d'où le bloc try/catch qui entoure le code.

Voilà, rien de plus à faire, vous êtes connectés à votre base de données.

Apparté : si vous ne voyez toujours pas vos messages d'erreur, ouvrez le fichier php.ini et vérifiez que vous avez les lignes error_reporting = On et error_reporting = e_all. Il faudra redémarrer Wamp après avoir modifié le fichier.

Créez un nouveau script PHP et reprenez le code ci-dessus pour vous connecter à votre base nintendo en l'adaptant à vos paramètres. Testez. Si tout se passe bien, vous devriez avoir simplement... une page blanche !


							

Exécuter une requête simple

Pour exécuter des requêtes classiques, vous pouvez utiliser les méthodes query() et exec().

query() retourne un jeu de résultats sous la forme d'un objet PDOStatement. exec() retourne uniquement le nombre de lignes affectées. On utilisera donc query() pour des requêtes de sélection (SELECT) et exec() pour des requêtes d'insertion (INSERT), de modification (UPDATE) ou de suppression (DELETE).

Par exemple, pour récupérer tous les jeux vidéos de notre base nintendo :


						$reponse = $pdo->query('SELECT * FROM game');

						$reponse->closeCursor(); //On stoppe le traitement de la requête
					

Vous pouvez tester et vous me direz que c'est bien joli mais vous avez toujours une page blanche ! Et oui, cette instruction permet d'exécuter le SELECT mais elle n'affiche rien. La variable $reponse contient entre autre les données récupérées. Entre autre, sinon ce serait trop simple. Notre variable $reponse est un objet de type PDOStatement. Pour afficher les données récupérées par la requête, vous devez utiliser les méthodes fetch() qui renvoie la ligne suivante du jeu de résultat ou fetchAll qui renvoie tous les résultats dans un tableau :


				$reponse = $pdo->query('SELECT * FROM game');

				$donnees = $reponse->fetch(); //renvoie le premier résultat
				var_dump($donnees); //Affiche de manière "brute" le contenu de la variable $donnees

				$donnees = $reponse->fetchAll(); //renvoie tous les résultats dans un tableau
				var_dump($donnees); 

				$reponse->closeCursor();
					

Voilà, maintenant vous devriez retrouver vos jeux affichés sur votre page. Ce n'est pas très joli puisque l'affichage est produit par la fonction var_dump. Vous pouvez très bien utiliser une boucle pour parcourir les résultats et les afficher comme vous le voulez. Pour afficher seulement le nom de tous les jeux de la base dans une liste par exemple, voilà le script complet :





					

Créez un nouveau script PHP qui affiche le nom des jeux de la Switch uniquement et leur prix dans un tableau.


							

Utiliser des requêtes préparées (et éviter les injections SQL)

On a vu comment exécuter des requêtes toutes simples mais comment construire la requête en fonction de variables ? Par exemple, pour inclure des données saisies par l'utilisateur dans un formulaire d'inscription ou de recherche. Et bien vous pouvez très bien utiliser query mais vous vous exposeriez à une faille de sécurité très connue et très classique : les injections SQL.

Les injections SQL

Une injection SQL, c'est ça :

XKCD - Exploits of a mom - Exemple injections SQL

xkcd

On imagine que dans le script de l'école, on pouvait trouver cette requête :


						INSERT INTO Students VALUES ( '$name' );
					

En théorie, l'utilisateur fournit son nom, et la valeur est insérée dans la requête :


						INSERT INTO Students VALUES ( 'Robert' );
					

Ici, le petit Bob a un nom bien particulier : Robert'); DROP TABLE Students; --, et la requête donc est devenue :


						INSERT INTO Students VALUES ( 'Robert' );  DROP TABLE Students; --' )
					

La requête INSERT originale du script se termine avec le );, et l'utilisateur du formulaire ajoute une requête DROP TABLE qui aura pour effet d'effacer toute la table. Il a donc injecté une seconde requête après la première : c'est l'injection SQL.

Moralité ? Ne jamais faire confiance à l'utilisateur. Jamais. Jamais. Jamais. Dans un formulaire, vous attendez un nom, une date, une valeur numérique ? Vé-ri-fiez.

Pour vos requêtes, il existe plusieurs moyens de se prémunir de ce type d'attaque. Vous pouvez bien sûr vérifier et interdire des inputs qui contiendraient du code. On peut aussi, et c'est la solution communément adoptée, séparer les données peu sûres des requêtes. Vous voyez le principe des templates HTML qui séparent le code HTML des données qui seront contenues ? Et bien c'est le même principe !

Qu'est ce qu'une requête préparée ?

Vous retrouverez les requêtes préparées avec la plupart des langages et frameworks. Le principe est d'écrire les requêtes sans les données associées en remplaçant les valeurs par des placeholders. Concrètement :


						SELECT *
						FROM game
						WHERE id=?
					

Le point d'interrogation tient lieu de placeholder. Le SGBD pourra préparer cette requête, déterminer son plan d'exécution et dans une deuxième phase, les valeurs seront insérées pour l'exécution. Les valeurs, et rien d'autre, la requête ne pourra pas être modifiée, et aucune autre requête ne pourra être injectée.

En plus de vous protéger des injections SQL, les requêtes préparées sont aussi plus rapides dès lors qu'elles sont exécutées plus d'une fois : le SGBD aura déjà préparé la requête.

Les requêtes préparées avec PDO

Dans votre code, l'utilisation de requêtes préparées complexifie un tout petit peu la programmation. Il faudra suivre les deux phases : la préparation de la requête, puis son exécution avec les valeurs associées.




					
					

Créez un nouveau script PHP qui affiche le nom des jeux et leur prix dans un tableau lorsque leur prix est compris entre 20€ et 40€ en utilisant une requête préparée.


							

Chiffrer des données

Avant de se quitter et pour terminer sur le chapitre de la sécurité des données, on va parler de la gestion des mots de passe. L'actualité nous le démontre un peu trop souvent, il y aurait bien plus à dire... Une gestion saine des mots de passe est plutôt un bon début.

Un petit apparté pour commencer. Comme tous les développeurs avant vous, quand vous cherchez une solution, vous la cherchez le plus souvent sur Internet. Prenez garde, et pour la sécurité en particulier, à vérifier vos sources. Beaucoup de posts, sujets de blogs, cours, voire pages de documentation sont obsolètes. Ils n'exploitent donc pas toutes les possibilités les plus récentes d'un langage, d'une API ou d'un framework. Vous manquerez peut-être une meilleure solution en terme de performances, de maintenabilité, de compatibilité si vous ne vérifiez pas qu'une portion de code croisée au détour d'une recherche Google est à jour.

Alors bien sûr, si vous cherchez, c'est que vous ne savez pas :) Comment faire alors ? La date d'un sujet n'est pas forcément un bon indicateur puisque la personne qui rédige pourrait bien ne pas avoir mis à jour ses connaissances depuis 10 ans. À contrario, sur une vieille techno, un post vieux de 15 ans peut très bien être correct. La référence devrait toujours être la documentation officielle du langage/framework/whatever. Vérifiez les pages des méthodes et des classes utilisées. Consultez d'autres sources, lisez les commentaires. En matière de sécurité, c'est indispensable. Si vous usez d'outils obsolètes, vous vous exposez à des failles de sécurité qui sont pourtant peut-être déjà comblées !

Si vous travaillez en back-end surtout, prenez les devants, consacrez du temps régulièrement à lire les actualités des outils que vous utilisez, que ce soit un langage, un système de gestion de base de données ou que sais-je. Non seulement vous vous tiendrez informés des potentielles failles de sécurité découvertes dans vos outils et pourrez apporter les corrections nécessaires mais vous pourrez aussi exploiter les dernières fonctionnalités. Et puis ça accompagne très bien le café du matin.

Résumez : lisez la doc !

Les fonctions de hachage

Et revenons à nos moutons, le cas particulier des mots de passe. Jamais ô grand jamais on ne stockera un mot de passe en clair, tel quel, dans une base de données. Imaginez, cela permettrait à quiconque ayant accès à la base de se connecter à un compte utilisateur. Et si la base de données est volée.. aïe !

On stockera les mots de passe chiffrés par un algorithme (ou fonction) de hachage. Explication : lors de l'inscription d'un utilisateur, on calculera la clé correspondant au mot de passe fourni. Cette clé est une suite de caractères qui est déterminée par l'algorithme. Dans un monde idéal sans faille, cette clé est unique et ne permet pas de retrouver le mot de passe original. Le chemin inverse clé -> mot de passe n'est pas possible. On parle aussi de signature ou encore d'empreinte. Par la suite, lorsque l'utilisateur se connectera, on vérifiera si la clé du mot de passe qu'il saisit est identique à la clé enregistrée dans la base.

Les fonctions de hachage permettent donc de comparer des données sans les connaître. Si les données sont identiques, alors les clés sont identiques. Si les données sont différentes, alors leurs clés sont différentes.

Différents algorithmes existent. Vous avez peut être entendu parler de l'algorithme md5 ou la famille sha. Il ne faut pas les utiliser pour chiffrer des mots de passe même si nombre de vieilles pages web vous en parleront. Ce sont des algorithmes rapides. Un hackeur mettant la main sur votre base de données et donc sur les clés des mots de passe de vos utilisateurs pourrait ainsi calculer des milliards de clés par seconde pour retrouver les mots de passe originaux. De plus, pour md5 et sha-1, des collisions sont possibles : deux chaînes générant la même clé. Gênant pour des mots de passe ! Mieux vaut préférer des algorithmes lents. Il faudra des lustres au malotru qui a piqué la base de données pour calculer des mots de passe et forcer l'accès à un compte. Actuellement, la solution couramment adoptée et conseillée est l'algorithme bcrypt.

Par exemple, pour le mot de passe toto (très mauvaise idée de mot de passe en passant), si on utilise l'algorithme bcrypt, on obtient la clé : $2y$10$eHzb1Hnq5Ifh3dnczRSc4.aNuTuMusXU3H.6Qh4YNdK2OHFOSSgsq. C'est cette clé qu'on stockera dans notre base de données. Par la suite, lorsque l'utilisateur tentera de se connecter, on vérifiera si la clé du mot de passe saisi correspond bien à $2y$10$eHzb1Hnq5Ifh3dnczRSc4.aNuTuMusXU3H.6Qh4YNdK2OHFOSSgsq. On ne connaîtra ainsi jamais le mot de passe de l'utilisateur. Lui seul le connaît.

En PHP, on utilisera deux fonctions pour chiffrer une chaine de caractères et vérifier si deux signatures sont identiques à partir de PHP 5.5 : password_hash et password_verify. Elles simplifient grandement la tâche. Si vous travaillez avec une version inférieure à PHP5.5 (j'ai un doute quant aux machines de l'IUT en écrivant ces lignes), il faut penser à militer pour une mise à jour.

Chiffrer des données

Pour chiffrer les données, on utilise password_hash (doc). Cette fonction permet de créer la clé de hachage pour un mot de passe. L'algorithme utilisé par défaut actuellement est bcrypt. Si vous lisez la doc, vous noterez qu'il est mentionné que l'algorithme utilisé peut être amené à évoluer. Bcrypt génère des clés d'une longueur de 60 caractères et pour parer aux évolutions, la doc PHP conseille de réserver 255 caractères aux mots de passe en base de données.

Pour l'utiliser, c'est plutôt simple, on fournit à la fonction le mot de passe à chiffrer et l'algo à utiliser :


						$cle = password_hash("toto", PASSWORD_DEFAULT);
						echo $cle;
						//Affiche $2y$10$eHzb1Hnq5Ifh3dnczRSc4.aNuTuMusXU3H.6Qh4YNdK2OHFOSSgsq
					

Et c'est donc cette valeur de 60 caractères que l'on stockera en base de données.

Vous allez créer un formulaire d'inscription tout simple demandant à l'utilisateur son login et mot de passe et enregistrerez ensuite ces informations correctement en base de données.

  1. Dans PhpMyAdmin, créez une nouvelle table user dans votre base de données nintendo.
  2. Créez les colonnes login, email et password dans votre table user en utilisant les types appropriés.
  3. Créez un script PHP qui comportera un formulaire demandant à l'utilisateur son login, son email et son mot de passe. Lorsque le formulaire est validé, le script PHP enregistre le nouvel utilisateur en base de données en chiffrant le mot de passe et affiche un message de réussite.

							

Vérifier le mot de passe

Maintenant que des utilisateurs peuvent s'inscrire, il serait bien qu'ils puissent aussi se connecter. Pour cela, il faudra pouvoir vérifier que le mot de passe qu'ils saisissent pour se connecter est identique au mot de passe saisi à l'inscription.

Pour se faire, on utilise la fonction password_verify (doc). On lui fournit en paramètre le mot de passe à tester et la clé (celle qui aura été enregistrée en base) et elle renverra vrai si ils correspondent, faux sinon.


						$cle = '$2y$10$eHzb1Hnq5Ifh3dnczRSc4.aNuTuMusXU3H.6Qh4YNdK2OHFOSSgsq';

						if (password_verify('toto', $cle)) {
						    echo 'Les mots de passe correspondent.';
						} else {
						    echo 'Echec : les mots de passe sont différents.';
						}
					

Vous allez maintenant créer le formulaire de connexion pour se connecter à son compte. Vous devrez :

  1. Créer un formulaire de connexion demandant à l'utilisateur son login et son mot de passe
  2. Créer un script PHP qui :
    • récupère la clé du mot de passe correspondant au login dans la base de données
    • vérifie si le mot de passe saisi pour la connexion correspond à la clé
    • affiche un message de succès ou échec à l'utilisateur

À ce stade, vous devriez voir qu'on pourrait stocker les jeux possédés par un utilisateur sans trop de travail supplémentaire et aboutir à un petit gestionnaire de jeux vidéo. Si vous avez envie de vous y atteler, allez-y :)