68 Chapitre 2. Techniques de base <input type=’hidden ’ name=’envoyer ’ value =’1’/> < table> < tr><th>Destinataire :</th> <td><input type =’text ’ size =’40’ name=’destinataire ’/></td> </ tr> < tr><th>Sujet :</th> <td><input type =’text ’ size =’40’ name=’sujet ’/></td> </ tr> < tr><th>Message :</th> <td>< textarea rows=’20’ cols =’40’ name=’message ’></ textarea> </td> </ tr> </ table> <input type=’submit ’ value=’Envoyer ’/> </form> À l’entrée du script, le processeur PHP décrypte les données provenant du formulaire et transférées dans le message HTTP, puis les place dans les tableaux $_POST ou $_GET selon le mode de transmission choisi. On peut également utiliser systématiquement le tableau $_REQUEST qui fusionne les deux précédents (plus le tableau $_COOKIES). À ce stade, PHP peut effectuer ou non une transformation consistant à préfixer toutes les apostrophes simples ou doubles par la barre oblique inverse « \ ». Ce comportement est déterminé par l’option de configuration magic_quotes_gpc,et motivé par l’insertion fréquente des données provenant de formulaires dans des requêtes SQL. Un des points épineux dans la manipulation de chaînes de caractères insérées ou lues dans une base MySQL est en effet la présence d’apostrophes. Prenons l’exemple suivant : INSERT INTO FilmSimple (titre , annee , nom_realisateur , prenom_realisateur , annee_naissance) VALUES ( ’L ’ ours ’ , 1988, ’Annaud ’ , ’Jean−Jacques ’ , 1943) MySQL distingue les valeurs grâce aux apostrophes simples « ’ ». Si une valeur contient elle-même une apostrophe, comme « L’ours » dans l’exemple ci-dessus, MySQL est perdu et produit un message d’erreur. La bonne syntaxe est : INSERT INTO FilmSimple (titre , annee , nom_realisateur , prenom_realisateur , annee_naissance) VALUES ( ’L\ ’ ours ’ , 1988, ’Annaud ’ , ’Jean−Jacques ’ , 1943) La présence d’un caractère « \ » devant l’apostrophe (on parle « d’échappe- ment ») permet à MySQL d’interpréter correctement cette dernière comme faisant partie de la chaîne. Quand l’option magic_quotes_gpc vaut On, un titre comme L’ours sera automatiquement représenté par la valeur L\’ours dans le script recevant les données. On pourra donc l’insérer tel quel dans une requête SQL. Cependant, comme le montre l’application que nous sommes en train de créer, une chaîne de caractères peut être utilisée dans bien d’autres contextes que SQL, et 2.2 Traitement des données transmises par HTTP 69 « l’échappement » des apostrophes par des barres obliques devient inutile et gênant. Il faut alors se poser sans cesse la question de la provenance de la variable, de la configuration courante de PHP, et de la nécessité ou non d’utiliser l’échappement. C’est encore plus ennuyeux quand on écrit des fonctions puisqu’il faut déterminer si les paramètres peuvent ou non provenir d’une transmission HTTP, et si oui penser à uniformiser, dans les appels à la fonction, la règle d’échappement à utiliser. Depuis la parution de PHP 5, les concepteurs et distributeurs du langage semblent renoncer à cet échappement automatique. Le problème est de risquer de se trouver dans une situation où certain serveurs pratiquent l’échappement et d’autres non. Le seul moyen pour régler le problème une fois pour toutes et de normaliser systé- matiquement les données HTTP. La politique adoptée dans ce livre (vous êtes libre d’en inventer une autre bien entendu) consiste à tester, à l’entrée de tout script, si le mode d’échappement automatique est activé. Si oui, on supprime cet échappement, pour toutes les chaînes transmises, avec la fonction stripSlashes(). On pourra alors considérer par la suite que les données HTTP sont représentées normalement, comme n’importe quelle autre chaîne de caractères manipulée dans le script. Voici la fonction qui effectue cette opération sur chacun des tableaux contenant d’une part des données transmises en mode get ou post, d’autre part des cookies. Exemple 2.10 exemples/Normalisation.php : Traitement des tableaux pour supprimer l’échappement automatique <?php // Application de la suppression des échappements , si nécessaire , // dans tous les tableaux contenant des données HTTP require_once("NormalisationHTTP.php") ; function Normalisation () { // Si l ’on est en échappement automatique , on rectifie if ( get_magic_quotes_gpc ()) { $_POST = NormalisationHTTP ($_POST) ; $_GET = NormalisationHTTP ($_GET) ; $_REQUEST = NormalisationHTTP ($_REQUEST) ; $_COOKIE = NormalisationHTTP ($_COOKIE) ; } } ?> La fonction get_magic_quotes_gpc() indique si l’échappement automatique est activé. On parcourt alors les tableaux concernés et traite chaque valeur 1 avec stripSlashes() qui supprime les « \ ». Dans le parcours lui-même, il faut prendre 1. On ne traite pas la clé de chaque élément, en considérant qu’une clé ne devrait pas contenir d’apostrophes. 70 Chapitre 2. Techniques de base en compte le fait qu’un élément du tableau peut constituer lui-même un tableau imbriqué (cas par exemple d’un formulaire permettant de saisir plusieurs valeurs pour un champ de même nom, voir page 46). Une manière simple et naturelle de parcourir les tableaux imbriqués sans se soucier du nombre de niveaux est d’appeler récursive- ment la fonction de normalisation NormalisationHTTP(), donnée ci-dessous. Exemple 2.11 exemples/NormalisationHTTP.php : Parcours récursif des tableaux pour appliquer stripSlashes(). <?php // Cette fonction supprime tout échappement automatique // des données HTTP dans un tableau de dimension quelconque function NormalisationHTTP( $tableau ) { // Parcours du tableau foreach ($tableau as $cle => $valeur) { if (!is_array($valeur)) // c’est un élément: on agit $tableau [$cle ] = stripSlashes ($valeur); else // c’est un tableau : on appelle récursivement $tableau [$cle ] = NormalisationHTTP($valeur); } return $tableau ; } ?> La construction foreach utilisée ici est très pratique pour parcourir un tableau en récupérant à la fois l’indice et la valeur de chaque entrée. On peut noter que cette fonction prend en entrée un tableau et produit en sortie une copie dans laquelle les échappements éventuels ont été supprimés. Il est possible, si l’on considère que ces copies sont pénalisantes, de traiter les paramètres par référence en les préfixant par «&». 2.2.2 Contrôle des données HTTP La seconde tâche à effectuer en recevant des données d’un formulaire est le contrôle des données reçues. Cela signifie, au minimum, 1. le test de l’existence des données attendues, 2. un filtrage sur ces données, afin de supprimer des caractères parasites qui pourraient infecter l’application; 3. et enfin le contrôle de quelques caractéristiques minimales sur les valeurs. On n’insistera jamais assez sur le fait qu’un script PHP est un programme que le monde entier peut appeler en lui passant n’importe quoi. Bien entendu la majorité des utilisateurs du Web a bien autre chose à faire que d’essayer de casser votre application, mais il suffit d’un malveillant pour créer des problèmes, et de plus ces 2.2 Traitement des données transmises par HTTP 71 attaques sont malheureusement automatisables. Un jour ou l’autre vous serez amenés à vous poser la question de la robustesse de vos scripts. Le filtrage des données en entrée, en particulier, est très important pour les sécuriser. La fonction ci-dessous est une version minimale des contrôles à effectuer. Elle repose pour les contrôles sur les fonctions isSet() et empty() qui testent res- pectivement l’existence d’une variable et la présence d’une valeur (chaîne non vide). Pour le filtrage la fonction utilise htmlSpecialChars() qui remplace les caractères marquant une balise (soit « < », « > » et « & ») par un appel d’entité (soit, respectivement, <, > et &). On peut également envisager de supprimer totalement les balises avec la fonction strip_tags(). L’injection de balises HTML dans les champs de formulaires est une technique classique d’attaque d’un site web. La fonction prend en entrée un tableau contenant les données, renvoie true si elles sont validées, et false sinon. Remarquer que le tableau $mail est passé par référence pour permettre sa modification suite au filtrage. On pourra utiliser cette fonction en lui passant le tableau $_POST pour valider la saisie du formulaire précédent, ainsi que tout autre tableau dont on voudrait contrôler le contenu selon les mêmes règles. Il est toujours préférable de concevoir des fonctions les plus indépendantes possibles d’un contexte d’utilisation particulier. Exemple 2.12 exemples/ControleMail.php : Ébauche de contrôle des données <?php // Fonction contrôlant l ’entrée de l ’application e−mail . function ControleMail (&$mail) { // Le tableau en paramètre doit contenir les entrées : // destinataire , sujet et message. Vérification . if (! isSet($mail[ ’ destinataire ’])) { echo "Pas de destinataire!"; return false ;} else $mail[ ’ destinataire ’] = htmlSpecialChars($mail [ ’destinataire ’]) ; if (! isSet($mail[ ’sujet ’ ])) { echo "Pas de sujet !" ; return false ;} else $mail[ ’ sujet ’ ] = htmlSpecialChars($mail [’sujet’]); if (! isSet ($mail [ ’message ’]) ) { echo "Pas de message!" ; return false ;} else $mail[ ’message ’ ] = htmlSpecialChars($mail [ ’message ’]) ; // On vérifie que les données ne sont pas vides if ( empty($mail[ ’destinataire ’])) { echo "Destinataire vide!"; return false ;} if ( empty($mail[ ’sujet ’])) { echo "Sujet vide !" ; return false ;} if ( empty($mail[ ’ message ’ ]) ) { echo "Message vide !" ; return false ;} 72 Chapitre 2. Techniques de base // Maintenant on peut / doit également faire des contrôles // sur les valeurs attendues: destinataire , sujet , message. // Voir les exercices pour des suggestions . return true ; } ?> On pourrait envisager beaucoup d’autres contrôles à effectuer, certains étant décrits dans le document d’exercices disponible sur le site. Les contrôles s’appuient fréquemment sur la vérification du format des données (comme, typiquement, l’adresse électronique) et nécessitent le recours aux expressions régulières qui seront présentées page 87. Dans toute la suite de ce livre, j’omets le plus souvent de surcharger le code par des contrôles répétitifs et nuisant à la clarté du code. Le filtrage et le contrôle des données en entrée font partie des impératifs de la réalisation d’un site sensible : reportez-vous au site php.net pour des recommandations à jour sur la sécurité des applications PHP. 2.2.3 Comment insérer dans la base de données : insertion dans MySQL Voyons maintenant comment effectuer des insertions dans la base à partir des données reçues. Il faut tout d’abord créer une table, ce qui se fait avec le script SQL suivant : Exemple 2.13 exemples/Mail.sql : Création de la table stockant les e-mails # # Création d ’une table pour stocker des e−mails # CREATE TABLE M a i l ( i d _ m a i l INT AUTO_INCREMENT NOT NULL , d e s t i n a t a i r e VARCHAR( 4 0 ) NOT NULL , s u j e t VARCHAR ( 4 0 ) NOT NULL , message TEXT NOT NULL, date_envoi DATETIME, PRIMARY KEY ( i d _ m a i l ) ) ; Petite nouveauté : on trouve dans la table Mail une option AUTO_INCREMENT, spécifique à MySQL. Cette option permet d’incrémenter automatiquement l’attribut id_mail à chaque insertion. De plus, cet attribut doit être déclaré comme clé primaire, ce qui signifie qu’il ne peut pas prendre deux fois la même valeur parmi les lignes de la table. On peut insérer une ligne dans Mail sans indiquer de valeur pour id_mail, déterminée automatiquement par MySQL. . sujet et message. Vérification . if (! isSet($mail[ ’ destinataire ’])) { echo "Pas de destinataire!"; return false ;} else $mail[ ’ destinataire ’] = htmlSpecialChars($mail [ ’destinataire. isSet($mail[ ’sujet ’ ])) { echo "Pas de sujet !" ; return false ;} else $mail[ ’ sujet ’ ] = htmlSpecialChars($mail [’sujet’]); if (! isSet ($mail [ ’message ’]) ) { echo "Pas de. le plus souvent de surcharger le code par des contrôles répétitifs et nuisant à la clarté du code. Le filtrage et le contrôle des données en entrée font partie des impératifs de la réalisation