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.