La sécurité des applications Web en PHP
Transcription
La sécurité des applications Web en PHP
La sécurité des applications Web en PHP Olivier Bichler∗ 21 avril 2007 ∗ E-mail : [email protected]. 1 TABLE DES MATIÈRES 2 Table des matières I Les données transmises par le client 3 1 Contrôler la provenance des données 1.1 Register Globals . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.2 Les variables superglobales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 Contrôler la validité des données 2.1 Vérifier l’existance des données . . . . . . 2.2 Vérifier le type des données . . . . . . . . 2.2.1 Entier (integer) . . . . . . . . . . . 2.2.2 Nombre à virgule flottante (float) . 2.2.3 Booléen (boolean) . . . . . . . . . 2.2.4 Tableau (array) . . . . . . . . . . . 2.3 Contrôler le format des données . . . . . . 2.3.1 Les listes de valeurs . . . . . . . . 2.3.2 L’utilisation d’un masque . . . . . 2.3.3 L’interdiction de certaines valeurs 2.3.4 Le contrôle du code HTML / XML 2.4 Contrôler la pertinence des données . . . . 3 3 3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 4 5 5 5 6 6 6 6 7 8 8 10 3 Traiter les données 3.1 Les Magics Quotes . . . . . . . . . . . . . . 3.2 Interactions avec la base de donnée . . . . . 3.3 Interactions avec le contenu HTML / XML 3.3.1 Protéger le code HTML / XML . . . 3.3.2 Protéger un script client . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11 11 12 12 12 12 II L’authentification du client 12 4 Authentification HTTP 4.1 L’authentification HTTP Basic . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.2 L’authentification HTTP Digest . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.3 Les limites de l’authentification HTTP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12 12 13 13 5 Authentification applicative 13 Références 14 3 Première partie Les données transmises par le client Une règle est de ne jamais faire confiance aux données transmises par le client. « Client » regroupe la personne physique et bien sûr son navigateur, qui est sous son contrôle. 1 Contrôler la provenance des données La première étape consiste à identifier quelles données, accessibles en PHP: Hypertext Preprocessor (PHP), sont susceptibles d’être transmises ou altérées par le client et de quelle manière. 1.1 Register Globals register_globals est une directive de configuration de PHP, aujourd’hui obsolète mais encore disponible, qui permet de rendre directement accessibles les variables des méthodes GET, POST et des cookies dans l’espace global de PHP. Lorsque cette directive est active, l’espace des variables globales du script est « pollué » par les variables envoyées par le client sans qu’il y ait de moyen de les identifier ni d’en contrôler la provenance. Il n’est pas étonnant que cette directive, combinée à la non prise en compte des erreurs de type « notice », comme c’est encore le cas dans la configuration par défaut de PHP, est à l’origine d’innombrables failles de sécurité dans les applications écrites en ce langage. Un exemple simple est présenté listing 1 : la variable $authorized n’est pas initialisée et le développeur n’en est pas averti si error_reporting est configuré par défaut (E_ALL & ˜E_NOTICE) [2]. Ainsi, si la directive register_globals est active (comme cela était encore le cas il n’y a pas très longtemps dans la configuration par défaut de PHP), il suffit à un client mal intentionné de charger le script PHP avec la variable GET « authorized=1 » : script.php?authorized=1, pour avoir accès aux données sensibles. Listing 1 – Exemple de faille classique avec register_globals à On 1 2 3 4 5 6 7 8 9 <?php i f ( authenticated_user () ) { $authorized = true ; } i f ( $authorized ) { i n c l u d e ( ’ / h i g h l y / s e n s i t i v e / d a t a . php ’ ) ; } ?> Bien qu’aujourd’hui la directive register_globals soit désactivée par défaut et que la directive error_reporting soit à E_ALL dans la configuration recommandée de PHP, la plupart des hébergeurs continuent à travailler avec la configuration par défaut et laissent register_globals à On pour garder la compatibilité avec les anciens scripts PHP. Afin de rester compatible avec l’option register_globals à Off, de nombreux développeurs, plutôt que de réécrire proprement leurs applications, se contentent d’importer les variables GET, POST et cookie dans l’espace global, grâce aux fonctions PHP import_request_variables() ou extract () . Il est clair que cela ne change en rien les problèmes de sécurité que peut poser l’utilisation des variables clients dans l’espace global de PHP. Même si l’on pouvait garantir que toutes les variables utilisées par un script soit initialisées quelque soit le cas de figure, le fait de ne pas pouvoir identifier clairement l’origine des variables - script ou client - complique inutilement le debuggage et la maintenance des applications. 1.2 Les variables superglobales Depuis la version 4.1.0, PHP met en place des variables superglobales permettant d’accéder directement aux différentes données transmises via le protocole HTTP. 2 CONTRÔLER LA VALIDITÉ DES DONNÉES Variable $_SERVER $_GET $_POST $_COOKIE $_FILES $_REQUEST 4 Provenance Serveur HTTP : contient généralement les headers HTTP émis par le navigateur et l’URL de la requête, donc les données transmises par méthode GET Méthode GET : provenant d’une URL ou d’un formulaire HTML Méthode POST : provenant d’un formulaire HTML Cookies (stockés par le navigateur sur le disque dur du client) Méthode POST : provenant d’un formulaire HTML avec upload de fichier Inconnue : constitué du contenu des variables $_GET, $_POST et $_COOKIE (et $_FILES pour PHP antérieur à la version 4.3.0 [2]) Tab. 1 – Variables superglobales fournies par le protocole HTTP en PHP On notera que la variable $_REQUEST ne permet pas de connaître la provenance des données qu’elles contient. Je recommande pour cette raison de ne pas l’utiliser. Les anciennes variables HTTP Ces variables superglobales remplacent les anciennes variables qui étaient respectivement $HTTP_SERVER_VARS, $HTTP_GET_VARS, $HTTP_POST_VARS, $HTTP_COOKIE_VARS et $HTTP_POST_FILES ($_REQUEST n’avait pas d’équivalent). Ces anciennes variables, qui n’étaient pas superglobales, sont obsolètes et ne doivent plus être utilisées (elles ne sont d’ailleurs plus générées avec la configuration recommandée de PHP). 2 Contrôler la validité des données Le contrôle de la validité des données constitue une pierre angulaire de la sécurité d’une application Web, la règle d’or étant de ne jamais faire confiance à des données susceptibles de provenir ou d’être altérées par un client. Une faille très classique dûe à l’absence de contrôle sur les données envoyées par le client est illustrée listing 2. Ce script permet d’inclure en fait n’importe quel fichier et d’afficher son contenu lorsque ce n’est pas un script PHP. En effet, il est possible d’inclure un fichier de n’importe quel répertoire parent avec l’URL script.php?page=../admin par exemple. Enfin, il n’est pas difficile d’ignorer l’extension « .php » en terminant la variable page par le caractère NUL, qui marque la fin de chaîne de caractère : script.php?page=../site.log%00. Listing 2 – Faille classique de l’include 1 2 3 <?php i n c l u d e ( ’ / p a g e s / ’ . $_GET [ ’ page ’ ] . ’ . php ’ ) ; ?> 2.1 Vérifier l’existance des données Et oui, rien n’oblige le client à envoyer les données que l’on attend ! Il peut tout à fait supprimer des paramètres dans l’URL ou omettre de remplir certaines cases d’un formulaire. Dans la mesure où l’on travaille toujours avec un tableau, que ce soit $_GET, $_POST ou autre, il existe plusieurs méthodes pour contrôler l’existence d’une clef d’un tableau, comme cela est illustré listing 3, cependant vous noterez qu’aucune méthode n’est strictement équivalente. Listing 3 – Vérification de l’existance d’une donnée 1 2 3 4 5 6 7 8 9 10 11 12 13 <?php // Méthode 1 i f ( a r r a y _ k e y _ e x i s t s ( ’ v a r ’ , $_GET) ) { // La v a r i a b l e " v a r " e x i s t e ( e l l e p e u t v a l o i r NULL) } // Méthode 2 i f ( i s s e t ($_GET [ ’ v a r ’ ] ) ) { // La v a r i a b l e " v a r " e x i s t e e t n ’ e s t p a s é g a l à NULL } // Méthode 3 i f ( ! empty ($_GET [ ’ v a r ’ ] ) ) { 2 14 15 16 17 CONTRÔLER LA VALIDITÉ DES DONNÉES 5 // La v a r i a b l e " v a r " e x i s t e e t ne v a u t n i NULL , n i une c h a î n e v i d e , n i 0 } /∗ L e s méthodes 1 e t 2 s o n t d a n s n o t r e u t i l i s a t i o n i d e n t i q u e s , c a r i l n ’ e s t p a s p o s s i b l e d ’ a t t r i b u e r à une v a r i a b l e c l i e n t l a v a l e u r NULL , q u i e s t t o u t à f a i t p a r t i c u l i è r e en PHP . P r é f é r e z l a méthode 2 , q u i f a i t a p p e l à une s t r u c t u r e du l a n g a g e p l u t ô t qu ’ à une f o n c t i o n ∗/ 2.2 Vérifier le type des données En PHP, toutes les données transmises par le client sont à la base de type « string » ou des tableaux de « string ». Il ne faut donc pas confondre le type d’une variable et le type de la donnée qu’elle contient. Une variable de type « string » peut très bien représenter un entier, mais la fonction is_int () renverra toujours « false ». Une première étape dans le processus de validation des données est donc de déterminer quel est le type qu’elles représentent. 2.2.1 Entier (integer) Le listing 4 montre plusieurs possibilités pour vérifier que la donnée représentée par une variable est un entier. Listing 4 – Différents types de vérification du type integer 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 <?php i f ($_GET [ ’ v a r ’ ] == ( s t r i n g ) ( i n t ) $_GET [ ’ v a r ’ ] ) { // $_GET [ ’ v a r ’ ] e s t un i n t e g e r } // Code é q u i v a l e n t m a i s p l u s l o u r d du p o i n t de v u e p e r f o r m a n c e s i f ($_GET [ ’ v a r ’ ] == s t r v a l ( i n t v a l ($_GET [ ’ v a r ’ ] ) ) ) { // $_GET [ ’ v a r ’ ] e s t un i n t e g e r } // Code é q u i v a l e n t , r e q u i è r e l ’ e x t e n s i o n PHP " c t y p e " i f ( c t y p e _ d i g i t ($_GET [ ’ v a r ’ ] ) ) { // $_GET [ ’ v a r ’ ] e s t un i n t e g e r } // L ’ u t i l i s a t i o n d ’ une e x p r e s s i o n r é g u l i è r e e s t p o s s i b l e , m a i s c e l a e s t p a r t i c u l i è r e m e n t mal a d a p t é p o u r l e c o n t r ô l e de t y p e s s i m p l e s , d ’ a u t a n t p l u s qu ’ une e x p r e s s i o n r é g u l i è r e mal m a î t r i s é e p e u t c o m p o r t e r d e s f a i l l e s i f ( preg_match ( ’ /^[0 −9]+ $ /D ’ , $_GET [ ’ v a r ’ ] ) ) { // $_GET [ ’ v a r ’ ] e s t un i n t e g e r } // C e t t e e x p r e s s i o n r é g u l i è r e e s t FAUSSE i f ( preg_match ( ’ /^[0 −9]+ $ / ’ , $_GET [ ’ v a r ’ ] ) ) { // $_GET [ ’ v a r ’ ] n ’ e s t p a s f o r c é m e n t un i n t e g e r , c a r c a r a c t è r e de n o u v e l l e l i g n e "\ n" } ?> 2.2.2 il peut se t e r m i n e r par l e Nombre à virgule flottante (float) Le listing 5 montre plusieurs possibilités pour vérifier que la donnée représentée par une variable est un nombre à virgule flottante. Listing 5 – Différents types de vérification du type float 1 2 3 4 5 6 7 8 9 10 <?php i f ($_GET [ ’ v a r ’ ] == ( s t r i n g ) ( f l o a t ) $_GET [ ’ v a r ’ ] ) { // $_GET [ ’ v a r ’ ] e s t un f l o a t } // Code é q u i v a l e n t m a i s p l u s l o u r d du p o i n t de v u e p e r f o r m a n c e s i f ($_GET [ ’ v a r ’ ] == s t r v a l ( f l o a t v a l ($_GET [ ’ v a r ’ ] ) ) ) { // $_GET [ ’ v a r ’ ] e s t un f l o a t } ?> 2 CONTRÔLER LA VALIDITÉ DES DONNÉES 2.2.3 6 Booléen (boolean) Le listing 6 montre comment vérifier que la donnée représentée par une variable est un booléen, en fait un entier valant « 0 » ou « 1 ». Ce cas se rencontre très couramment dans les variables passées par méthode GET, pour activer ou non une option par exemple. Listing 6 – Vérification du type boolean 1 2 3 4 5 <?php i f ($_GET [ ’ v a r ’ ] == ( s t r i n g ) ( i n t ) ( b o o l ) $_GET [ ’ v a r ’ ] ) { // $_GET [ ’ v a r ’ ] e s t un b o o l e a n } ?> 2.2.4 Tableau (array) Lorsque l’on attend un tableau, la vérification peut être faite simplement avec la fonction PHP Cela est utile lorsque l’on a des champs de sélection multiple dans un formulaire HTML ou lorsque l’on transmet les données dans l’URL de la forme script.php?var[]=10&var[a]=abc. Cette vérification est un préalable au contrôle des données dans le tableau, qui rappelons-le, sont de type « string » ou tableaux de « string ». is_array () . 2.3 Contrôler le format des données La détermination du type des données est un préalable à un examen de leur format : dans le cas d’un entier par exemple, peut-il être nul ? Doit-il être positif ? Dans le cas d’une chaîne de caractère, que doit représenter celle-ci, une adresse E-mail ? Peut-elle contenir certains caractères ? etc... Nous passerons sur les contrôles triviaux pouvant être effectués sur les entiers et les nombres à virgule flottante. Cela n’empêche pas de devoir faire ces contrôles lorsque qu’un nombre ne peut prendre qu’une certaine plage de valeur. 2.3.1 Les listes de valeurs Souvent une variable client ne peut prendre qu’un certain nombre de valeurs, comme c’est le cas pour les champs de sélection dans les formulaires HTML, ou encore pour les pages d’un site lorsque celles-ci sont toutes chargées à partir de l’index avec un include() . Plusieurs méthodes de validation sont alors possibles dont les plus courantes sont décrites dans le listing 7. Listing 7 – Traitement des listes de valeurs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 <?php // Méthode 1 : a d a p t é e l o r s q u e l ’ on u t i l i s e l e même t a b l e a u p o u r c r é e r l e champ de s é l e c t i o n HTML $pages = array ( ’ contact ’ , ’ products ’ , ’ search ’ , ’ support ’ ) ; i f ( i n _ a r r a y ($_GET [ ’ page ’ ] , $ p a g e s ) ) { i n c l u d e ( ’ p a g e s / ’ . $_GET [ ’ page ’ ] . ’ . php ’ ) ; } // Méthode 2 : a d a p t é e l o r s q u ’ un g r a n d nombre de v a l e u r s e s t p o s s i b l e i f ( preg_match ( ’ /^( c o n t a c t | p r o d u c t s | s e a r c h | s u p p o r t ) $ /D ’ , $_GET [ ’ page ’ ] ) ) { i n c l u d e ( ’ p a g e s / ’ . $_GET [ ’ page ’ ] . ’ . php ’ ) ; } // Méthode 3 : a d a p t é e l o r s q u e un t r a i t e m e n t d i f f é r e n t e s t e f f e c t u é e s e l o n l a v a l e u r de la variable i f ($_GET [ ’ t a s k ’ ] == ’ f o r m P r i n t ’ ) { // T r a i t e m e n t . . . } e l s e i f ($_GET [ ’ t a s k ’ ] == ’ f o r m P r o c e s s ’ ) { // T r a i t e m e n t . . . } e l s e i f ($_GET [ ’ t a s k ’ ] == ’ formShow ’ | | $_GET [ ’ t a s k ’ ] == ’ f o r m C o m p l e t e ’ ) { // T r a i t e m e n t . . . } /∗ La s t r u c t u r e s w i t c h p e r m e t de g a g n e r en c l a r t é l o r s q u ’ i l y a un nombre i m p o r t a n t de p o s s i b i l i t é s , m a i s s o n t u t i l i s a t i o n n ’ e s t p a s t o u j o u r s t r è s a d a p t é e ∗/ 2 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 CONTRÔLER LA VALIDITÉ DES DONNÉES 7 s w i t c h ($_GET [ ’ t a s k ’ ] ) { case ’ f o r m P r i n t ’ : // T r a i t e m e n t . . . break ; case ’ formProcess ’ : // T r a i t e m e n t . . . break ; c a s e ’ formShow ’ : case ’ formComplete ’ : // T r a i t e m e n t . . . break ; default : break ; } ?> 2.3.2 L’utilisation d’un masque Dès lors que le format d’une donnée est un tant soit peu complexe et qu’il devient impossible de lister toutes les valeurs possibles, les expressions régulières peuvent devenir très utiles, afin de créer des « masques » auquelles les données doivent se conformer. Attention toutefois, la manipulation des expressions régulières est délicate et conduit souvent à des masques erronées lorsqu’elles ne sont pas bien maîtrisées. Inclusion de fichiers De nombreux portails en PHP adoptent le système d’un index principale qui inclut les différentes pages du site, avec des URLs du type index.php?page=products. L’enjeu est alors de filtrer les valeurs de page afin de ne permettre l’inclusion que de certains fichiers, sans toutefois utiliser une liste de valeur fermée, qui nécessiterait une modification à chaque ajout de page. Le listing 8 montre un exemple d’un tel filtre, qui est la bonne méthode pour éviter une faille telle que celle présentée au listing 2. Listing 8 – Contrôle d’une variable d’inclusion de fichiers 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <?php // C e t t e e x e m p l e c o m p l e t i n c l u t d é j à du t r a i t e m e n t de d o n n é e i f ( i s s e t ($_GET [ ’ page ’ ] ) ) { i f ( preg_match ( ’ / ^ [ a−z_ ] [ a−z0 −9_\ ␣\/\−]+$ / Di ’ , $_GET [ ’ page ’ ] ) ) $ f i l e = $_GET [ ’ page ’ ] . ’ . php ’ ; else $ f i l e = NULL ; } else $ f i l e = ’ i n d e x . php ’ ; // T r a i t e m e n t d e s d o n n é e s . . . i f ( $ f i l e !== NULL && f i l e _ e x i s t s ( ’ p a g e s / ’ . $ f i l e ) ) { require ( ’ pages / ’ . $ f i l e ) ; } else { echo ’ Page ␣ non ␣ t r o u v é e ’ ; } ?> Format d’une date / d’une heure Les listings 9 et 10 montrent des expressions régulières simples pour contrôler le format d’une date et d’une heure. Listing 9 – Format d’une date 1 2 3 4 5 <?php i f ( preg_match ( ’ /^[0 −9]{4}\ −[0 −9]{2}\ −[0 −9]{2} $ /D ’ , $_POST [ ’ d a t e ’ ] ) ) { // La d a t e a l e bon f o r m a t } ?> 2 CONTRÔLER LA VALIDITÉ DES DONNÉES 8 Listing 10 – Format d’une heure 1 2 3 4 5 <?php i f ( preg_match ( ’ / ^ [ 0 − 9 ] { 2 } \ : [ 0 − 9 ] { 2 } \ : [ 0 − 9 ] { 2 } $ /D ’ , $_POST [ ’ t i m e ’ ] ) ) { // L ’ h e u r e a l e bon f o r m a t } ?> Format d’une adresse E-mail Le listing 11 permet de vérifier qu’une adresse E-mail a le bon format. Notez que l’expression régulière proposée est plus restrictive que ce qui est autorisé par la norme, cependant un certain nombre de caractères, bien que valides dans l’absolu, ne sont en pratique pas permis par bon nombre de clients mails pour un certain nombre de raisons et ont donc aucune chance de se retrouver dans une adresse E-mail valide1 . Listing 11 – Format d’une adresse E-mail 1 2 3 4 5 <?php i f ( preg_match ( ’ /^[&\ ’\∗\+0 −9a−z_\ −]+(\.[&\ ’\∗\+0 −9 a−z_\−]+)∗@ [ a−z0 −9\ −]+(\.[ a−z0 −9\−]+)+ $ / Di ’ , $_POST [ ’ e m a i l ’ ] ) ) { // L ’ a d r e s s e E−m a i l a l e bon f o r m a t } ?> 2.3.3 L’interdiction de certaines valeurs Lorsqu’il n’est pas possible de lister toutes les valeurs possibles, ou de leur apposer un masque, il peut rester nécessaire d’interdire certaines valeurs ou certains caractères. Quelques techniques sont présentées listing 12. Cette technique est parfois utilisée pour contrôler l’inclusion de fichiers, pour interdire l’accès aux fichiers d’un répertoire parent par exemple, en excluant les caractères "/" et "." du début de chaîne. Je ne conseille pas de procéder ainsi, mais plutôt d’utiliser une liste de valeurs ou un masque le cas échéant, qui assureront toujours un contrôle plus strict et plus sûr. Listing 12 – Interdiction de certaines valeurs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 <?php i f ( s t r p o s ($_GET [ ’ v a r ’ ] , ’ admin ’ ) === FALSE ) { // $_GET [ ’ v a r ’ ] ne c o n t i e n t p a s " admin " } // Ce c o d e e s t FAUX : i f ( ! s t r p o s ($_GET [ ’ v a r ’ ] , ’ admin ’ ) ) { // $_GET [ ’ v a r ’ ] p e u t commencer p a r " admin " ! } if ( ! preg_match ( ’ /[\/\ >]/ ’ , $_GET [ ’ v a r ’ ] ) ) { // $_GET [ ’ v a r ’ ] ne c o n t i e n t p a s l e s c a r a c t è r e s "/" e t ">" } ?> 2.3.4 Le contrôle du code HTML / XML Dans des forums de discussion, ou dans des commentaires d’articles, de billets, on peut vouloir autoriser le code HTML, du moins de manière limité, pour que les utilisateurs puissent mettre en forme leurs textes. Cela pose un certain nombre de contraintes : – Il faut pouvoir autoriser certaines balises et pas d’autres ; – De même, il faut pouvoir autoriser certains attributs de balises et pas d’autres ; – Il faut pouvoir contrôler le contenu des attributs, par exemple l’attribut « href » peut contenir un javascript ; – Si l’on autorise la balise <div>, qu’est ce qui empêche le client de fermer la balise sans l’avoir ouverte et ainsi casser la mise en page du site ? 1 Pour plus d’informations sur les caractères que peut contenir une adresse E-mail, voir http://www.remote.org/jochen/ mail/info/chars.html 2 CONTRÔLER LA VALIDITÉ DES DONNÉES 9 – Plus généralement, comment assurer que le code HTML fourni par le client soit valide et conforme aux recommandations HTML ? On pourrait être tenté d’utiliser des expressions régulières, comme cela se pratique, mais pour peu que l’on souhaite autoriser un certain nombre de balises, avec en plus la nécessité de valider le code HTML, ça devient vite compliqué, pour ne pas dire impossible. La bonne solution est d’utiliser un parseur XML permettant de valider un document XML à partir d’un DOCTYPE. Pour se faire, PHP 5 dispose en standard de tous les outils nécessaires avec l’extension « DOM » [2], qui est généralement proposée chez les hébergeurs PHP 5. Le listing 13 présente une fonction de validation de code XHTML, transposable à n’importe quel langage XML. Listing 13 – Valider un code XHTML avec PHP 5 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 <?php c l a s s ValidateXML { c o n s t Err_NotValidXHTML = −1; c o n s t E r r _ N o t V a l i d L i n k = −2; private $ v a l i d a t e E r r o r s = array () ; p u b l i c f u n c t i o n checkXHTML ( $body ) { $doc = new DOMDocument ; $doc −> r e s o l v e E x t e r n a l s = TRUE ; $ t h i s −> v a l i d a t e E r r o r s = a r r a y ( ) ; // C o n t r ô l e de l a v a l i d i t é du c o d e XHTML set_error_handler ( array ( $this , ’ v a l i d a t e E r r o r ’ ) ) ; $ i s V a l i d = @$doc −> loadXML ( ’<?xml ␣ v e r s i o n ="1.0" ␣ e n c o d i n g =" u t f −8"?> ’ . ’ <!DOCTYPE␣ h t m l ␣PUBLIC␣"−//W3C//DTD␣XHTML␣ 1 . 1 ␣ p l u s ␣MathML␣ 2 . 0 / /EN" ␣ " d t d / xhtml11 − mathml20−f l a t −l i m i t e d . d t d"> ’ . ’<h t m l ␣ x m l n s=" h t t p : / /www . w3 . o r g /1999/ x h t m l"> ’ . ’<head> ’ . ’ ␣␣< t i t l e >XHTML␣ c he c k </ t i t l e > ’ . ’</head> ’ . ’<body><d i v > ’ . $body . ’</ d i v ></body> ’ . ’</html> ’ ); $ i s V a l i d = @$doc −> v a l i d a t e ( ) && $ i s V a l i d ; restore_error_handler () ; if (! $isValid ) r e t u r n s e l f : : Err_NotValidXHTML ; else { // C o n t r ô l e du f o r m a t d e s l i e n s h y p e r t e x t e s $ r e g E x p = ’ /^( h t t p s ? | f t p ) \ : \ / \ / ’ // scheme ( s i m p l i f i e d ) . ’ ( ( [ ; \ : & \ = \ + \ $ , a−z0 −9\−_\ . \ ! ~ \ ∗ \ ’ \ ( \ ) ] | ( % [ 0 − 9 a−f ] { 2 } ) ) ∗@) ∗ ’ // u s e r i n f o . ’( ’ . ’ [ a−z ] [ a−z0 −9\ −]+(\.[ a−z0 −9\−]+)∗ ’ // hostname ( s i m p l i f i e d ) . ’| ’ . ’ [ 0 − 9 ] { 1 , 3 } \ . [ 0 − 9 ] { 1 , 3 } \ . [ 0 − 9 ] { 1 , 3 } \ . [ 0 − 9 ] { 1 , 3 } ’ // I P v 4 a d d r e s s . ’) ’ . ’ ( \ : [ 0 − 9 ] ∗ ) ∗ ’ // p o r t . ’ ( \ / | ( ; | ( [ \ : @&\=\+\$ , a−z0 −9\−_\ . \ ! ~ \ ∗ \ ’ \ ( \ ) ] | ( % [ 0 − 9 a−f ] { 2 } ) ) ∗ ) ∗ ) ∗ ’ // p a t h ( simplified ) . ’ ( \ ? ( [ ; \ / \ : @&\=\+\$ , a−z0 −9\−_\ . \ ! ~ \ ∗ \ ’ \ ( \ ) ] | ( % [ 0 − 9 a−f ] { 2 } ) ) ∗ ) ∗ ’ // q u e r y . ’ ( # ( [ ; \ / \ : @&\=\+\$ , a−z0 −9\−_\ . \ ! ~ \ ∗ \ ’ \ ( \ ) ] | ( % [ 0 − 9 a−f ] { 2 } ) ) ∗ ) ∗ ’ // f r a g m e n t . ’ $ / Di ’ ; f o r e a c h ( $doc −> getElementsByTagName ( ’ ∗ ’ ) as $ t a g ) { i f ( ( $ t a g −> tagName == ’ a ’ && ! preg_match ( $regEx p , $ t a g −> g e t A t t r i b u t e ( ’ h r e f ’ ) ) ) | | ( ( $ t a g −> tagName == ’ q ’ | | $ t a g −> tagName == ’ b l o c k q u o t e ’ | | $ t a g −> tagName == ’ i n s ’ | | $ t a g −> tagName == ’ d e l ’ ) && ! preg_match ( $regEx p , $ t a g −> getAttribute ( ’ cite ’ ) ) ) ) return s e l f : : Err_NotValidLink ; } } r e t u r n TRUE ; 2 10 } 56 57 58 59 60 61 62 63 64 65 66 CONTRÔLER LA VALIDITÉ DES DONNÉES public function getValidateErrors () { r e t u r n $ t h i s −> v a l i d a t e E r r o r s ; } public function v a l i d a t e E r r o r ( $errno , $ e r r s t r , $ e r r f i l e , $ e r r l i n e , $ e r r c o n t e x t ) { $ t h i s −> v a l i d a t e E r r o r s [ ] = s t r _ r e p l a c e ( a r r a y ( ’ DOMDocument : : loadXML ( ) : ␣ ’ , ’ DOMDocument : : v a l i d a t e ( ) : ␣ ’ ) , ’ ’ , $ e r r s t r ) ; } } ?> 2.4 Contrôler la pertinence des données La vérification du format des données ne garantit pas que celles-ci soient justes ou utilisables. Les dates « 2006-02-29 » ou « 2006-13-65 » n’existent pas, bien qu’elles aient le bon format. Il en va de même pour les adresses E-mails, les URLs etc... Bien qu’il soit souvent difficile de déterminer si une donnée est pertinente ou pas, il est dans certains cas possible, et parfois nécessaire, de s’assurer qu’elle ait un sens. Existence d’une date / d’une heure Les listings 14 et 15 montrent comment contrôler l’existence d’une date et d’une heure. Listing 14 – Existence d’une date 1 2 3 4 5 6 7 8 9 <?php i f ( preg_match ( ’ / ^ ( [ 0 − 9 ] { 4 } ) \ −([0 −9]{2}) \ −([0 −9]{2}) $ /D ’ , $_POST [ ’ d a t e ’ ] , $ r e g s ) ) { // La d a t e a l e bon f o r m a t i f ( checkdate ( $regs [ 2 ] , $regs [ 3 ] , $regs [ 1 ] ) ) { // La d a t e e x i s t e } } ?> Listing 15 – Existence d’une heure 1 2 3 4 5 6 7 8 9 <?php i f ( preg_match ( ’ / ^ ( [ 0 − 9 ] { 2 } ) \ : ( [ 0 − 9 ] { 2 } ) \ : ( [ 0 − 9 ] { 2 } ) $ /D ’ , $_POST [ ’ t i m e ’ ] , $ r e g s ) ) { // L ’ h e u r e a l e bon f o r m a t if ( ( $ r e g s [ 1 ] >= 0 && $ r e g s [ 1 ] < 2 4 ) && ( $ r e g s [ 2 ] >= 0 && $ r e g s [ 2 ] < 6 0 ) && ( $ r e g s [ 3 ] >= 0 && $ r e g s [ 3 ] < 6 0 ) ) { // L ’ h e u r e e x i s t e } } ?> Existence d’un fichier / d’une URL Lorsqu’il s’agit d’un fichier local, dont l’emplacement est donné par le client, il est toujours nécessaire de vérifier son existence, avec la fonction PHP file_exists () , avant d’aller plus loin. Vérifier l’existence d’une URL est toujours plus compliqué, pour plusieurs raisons : – L’URL peut exister mais être temporairement indisponible ; – L’URL peut exister mais produire une erreur (404 par exemple) ; – L’URL peut exister mais subir une redirection ; – Ouvrir une URL externe demande du temps et des ressources à PHP ; – PHP n’autorise pas toujours l’ouverture d’URLs externes. Existence d’une adresse E-mail Il en va de même pour une adresse E-mail, il existe des scripts (compliqués) qui permettent de contrôler l’existence d’une boîte E-mail, mais ils sont consommateurs de ressources et jamais fiables à 100%. Une technique simple et couramment utilisée pour contrôler l’existence d’une adresse E-mail, lors de la création d’un compte utilisateur par exemple, est d’exiger de l’utilisateur une confirmation de son inscription qu’il ne lui est possible de faire qu’avec des informations envoyées dans un E-mail à l’adresse qu’il a indiqué, comme un mot de passe ou une URL d’activation. 3 TRAITER LES DONNÉES 3 11 Traiter les données Le traitement des données client est essentiel dans toute application Web et sa négligeance est à l’origine des failles les plus courantes dans ce type d’application. 3.1 Les Magics Quotes PHP met en place par défaut un système de traitement automatique des données clients provenant des méthodes GET, POST et des cookies, contrôlé par la directive magic_quotes_gpc. Bien que celle-ci soit désormais désactivée dans la configuration recommandée de PHP, elle est encore active sur pratiquement tous les hébergeurs. Lorsque magic_quotes_gpc est à On, tous les caractères ’ (guillemets simples), " (guillemets doubles), \ (antislash) et NUL sont échappés avec un antislash, sauf quand magic_quotes_sybase est activé, dans quel cas seul les guillemets simples sont échappés avec un deuxième guillemet simple. D’autre part, si la directive de configuration magic_quotes_runtime est active, toutes les données provenant d’une source externe, y compris les bases de données et les fichiers texte, subirons le même traitement, dépendant également de magic_quotes_sybase. Je n’ai jamais vu de scripts utilisant cette directive, qui est fort heureusement configurable directement dans le script PHP, contrairement à magic_quotes_gpc. Les Magics Quotes, qui sont désormais désactivés dans la configuration recommandée de PHP, sont en fait une fausse bonne idée, qui était destinée entre autre à protéger automatiquement les données client pour les requêtes SQL, à la place du développeur amateur. Cependant, les Magics Quotes posent un certain nombre de problèmes : – Toutes les données sont protégées, alors que seules les données destinées à la base de données sont concernées. Ainsi, il faut systématiquement enlever les antislashs en trop des données enregistrées dans un fichier ou simplement réaffichées sur la page Web ; – Traiter toutes les données implique une perte de performance inutile et la nécessité de retraiter celles qui ne doivent pas être protégées encore plus ; – Seuls certains caractères sont échappés, alors que d’autres caractères peuvent poser problème selon le type de base de données utilisé ; – Toutes les bases de données ne protègent pas les données avec des antislashs ; – Cette directive désensibilise le développeur à un aspect crutial de la sécurité de son application. Je recommande donc de ne jamais se reposer sur les Magics Quotes. N’étant malheureusement pas possible de désactiver cette option directement dans le script PHP, il est nécessaire de traiter les données au cas par cas selon la configuration ou alors, pour simplifier leur traitement, de supprimer tous les Magics Quotes en début de script, comme le fait le listing 16 (notez qu’il n’est pas nécessaire de traiter la directive magic_quotes_sybase, car la fonction PHP stripslashes () s’adapte en fonction de celle-ci). Listing 16 – Supprimer les Magics Quotes 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <?php i f ( get_magic_quotes_gpc ( ) ) { f u n c t i o n a r r a y S t r i p s l a s h e s (& $ a r r a y ) { f o r e a c h ( $ a r r a y as &$ v a l u e ) { i f ( is_array ( $value ) ) a r r a y S t r i p s l a s h e s ( $value ) ; else $value = s t r i p s l a s h e s ( $value ) ; } } a r r a y S t r i p s l a s h e s ($_GET) ; a r r a y S t r i p s l a s h e s ($_POST) ; a r r a y S t r i p s l a s h e s ($_COOKIE) ; } i f ( g e t_ ma g i c _ q u o te s _ ru n t i me ( ) ) set_magic_quotes_runtime ( 0 ) ; ?> 12 3.2 Interactions avec la base de donnée Dès lors qu’une donnée client est destinée à interagir avec la base de donnée, via une requête SQL, il devient primordiale de la sécuriser. Pour se faire, des fonctions PHP spécifiques à chaque base de données existent : – mysql_real_escape_string() : protège les caractères spéciaux d’une commande MySQL, à préférer sur la fonction mysql_escape_string(), qui ne tient pas compte du jeu de caractères courant et qui est devenue obsolète [2] ; – sqlite_escape_string () : protège une chaîne de caractères pour utilisation avec SQLite ; – pg_escape_string() : protège une chaîne de caractères pour utilisation avec PostgreSQL ; – etc... De nombreux scripts se contentent d’utiliser la fonction addslashes () . C’est inadapté et insuffisant, il faut toujours utiliser les fonctions spécifiques prévues pour les bases de données ! Ces fonctions permettent de protéger des chaînes de caractères, mais ne doivent pas être utilisées protéger des nombres, ceux-ci n’étant pas entourés de guillements dans les requêtes SQL. Il est alors judicieux de forcer systématiquement le type de la variable, en « integer » ou en « float » selon le cas, avec la syntaxe PHP ( int ) $_GET[’id’] ou ( float ) $_GET[’id’]. Cela permet, même en cas d’absence de contrôle du type de donnée, de protéger les requêtes SQL contre des données impropres. 3.3 Interactions avec le contenu HTML / XML Les données fournies par le client sont couramment utilisées pour être réaffichées sur les pages Web, parfois après avoir été stockées dans la base de données. Ces données sont susceptibles de contenir du code HTML, qui pourrait « polluer » le code originel de la page Web, ce qui impose donc de les traiter. 3.3.1 Protéger le code HTML / XML La fonction de base en PHP pour protéger le code HTML ou XML est remplit très bien son rôle. 3.3.2 htmlspecialchars () et celle-ci Protéger un script client Il peut arriver que des données client soient utilisées par la suite dans un javascript par exemple, en tant que chaîne de caractère. Il est alors nécessaire de protéger ces données avec addcslashes ($_GET[’str’], "\’\\\n\r\x00"), ou htmlspecialchars ( addcslashes ($_GET[’str’], "\’\\\n\r\x00")) si le javascript est dans un attribut de balise HTML. Deuxième partie L’authentification du client 4 Authentification HTTP Ce système d’authentification est très simple à mettre en place puisqu’il disponible sur la plupart des serveurs HTTP et repose également sur le navigateur client. Il ne nécessite donc pas la mise en place de session ou de cookie : une fois l’utilisateur authentifié, le navigateur conserve en mémoire son identifiant et son mot de passe pour toute nouvelle requête nécessitant cette authentification durant la session de navigation. Il y a deux types d’authentification HTTP : « basic » et « digest ». 4.1 L’authentification HTTP Basic L’authentification HTTP Basic transmet à chaque requête l’identifiant et le mot de passe, séparés par le caractère : (deux-points), le tout encodé en base 64 [1]. L’identifiant et le mot de passe circulent donc à chaque requête pratiquement en clair, ce qui implique que ce type d’authentification est particulièrement sensible à une écoute du trafic. Ce type d’authentification est toutefois souvent utilisé conjointement à une connexion sécurisée type SSL, qui assure que la transmission, y compris celle du mot de passe, est cryptée. 5 AUTHENTIFICATION APPLICATIVE 4.2 13 L’authentification HTTP Digest Ce type d’authentification, à la différence de HTTP Basic, fonctionne avec un mécanisme de type « challenge / response » : le serveur envoie un challenge au client (data), qui répond par une valeur dérivée du challenge et du secret supposé partagé avec le serveur (secret, qui comprend l’identifiant et le mot de passe). L’algorithme utilisé pour la construction de la réponse du client est très simple, celle-ci étant constituée du hash MD5 de la concaténation de secret, du caractère : (deux-points) et de data. Il ne resterait plus qu’à préciser comment sont obtenu secret et data, mais pour cela, je vous invite à consulter la RFC2617 [1]. Cette méthode a notamment l’avantage d’être moins sensible à une écoute de trafic et est donc indiquée en l’absence de connexion sécurisée. En outre, de nombreuses considérations sur la sécurité de cette méthode sont détaillées dans la RFC2617 [1]. 4.3 Les limites de l’authentification HTTP Voici quelques reproches courants sur ce type d’authentification : – Impossibilité de personnaliser le formulaire d’authentification, qui dépend du navigateur ; – L’utilisateur doit fermer son navigateur pour mettre fin à sa session ; – Impossibilité d’entrer le mot de passe autrement qu’avec le clavier (ce qui peut poser problème pour des sites très sécurisés comme les banques). Son implémentation directe dans une application Web (c’est-à-dire sans passer par le serveur, un « .htaccess » avec Apache par exemple) est possible (PHP gère la méthode Digest depuis la version 5.1.0). Cependant, elle est peut vite devenir un casse-tête, notamment pour l’authentification HTTP Digest, car elle dépend également du serveur et du navigateur de l’utilisateur. 5 Authentification applicative L’authentification est gérée directement par l’application Web, le plus souvent via un formulaire HTML et un mécanisme de sessions (mais elle peut également, comme dit, implémenter une authentification HTTP). Nous allons voir comment mettre en place une méthod RÉFÉRENCES 14 Références [1] The Internet Society. RFC2617 - HTTP Authentication: Basic and Digest Access Authentication http: // www. ietf. org/ rfc/ rfc2617. txt , 1999. [2] PHP Documentation Group. PHP Manual http: // www. php. net/ manual/ , 2006.