1. Définition d'un format▲
Ce que nous appelons "format" est en fait l'ensemble des modifications apportées
à une donnée avant de commencer réellement à travailler dessus ou à
l'afficher.
Par exemple, transformer les chevrons ( < ) en entité HTML ( < ) est
un formatage de notre donnée.
Nous allons nous intéresser dans ce qui suit aux différents formats qu'une
donnée peut avoir tout au long de l'exécution du code PHP.
2. Théorie sur le format de stockage des données▲
La plupart des applications PHP ont besoin d'écrire / lire / modifier / supprimer des données dans une base SQL ou dans un fichier.
Le choix du format de représentation d'une donnée est important :
si vous cryptez votre donnée, vous devrez la décrypter pour la lire ;
de la même manière, si vous stockez votre donnée de manière "formatée", vous
devrez la "déformater" pour la modifier.
De plus, la plupart des formatages augmentent la longueur de la donnée formatée.
Donc si vous avez une base de données avec une colonne de taille X, et que vous
souhaitez stocker une donnée formatée, vous ne pourrez stocker dans la base que
X-F caractères, F étant le nombre de caractères nécessaires au formatage.
Il est difficile de dire à l'utilisateur de votre site Web qu'il peut saisir
moins de caractères dans son formulaire s'il utilise des guillemets, des
chevrons, ou d'autres métacaractères, parce que votre méthode de formatage va
augmenter la taille de sa donnée.
Nous conseillons donc de toujours stocker la donnée non formatée, dans son format brut.
Il est possible de stocker des données formatées pour des raisons de performances, afin d'éviter d'avoir à effectuer le formatage à chaque affichage, mais on prendra soin de ne jamais accéder au champ formaté pour le modifier et de garder une copie de la donnée non formatée pour cela.
3. Les différents formats▲
Pour mettre en évidence les différents formats, nous utiliserons des exemples
sur la chaîne simplifiée de test suivante :
a'a"a<a>a</a>a\a.
Une chaîne de test plus complète est donnée dans la dernière partie de l'article
().
Nous donnerons également les méthodes correspondantes en PHP pour formater les
données lorsqu'elles existent (de manière non exhaustive).
On pourra aussi se référer à la partie "Encodage des caractères" du
cours PHP de Yogui sur developpez.com pour plus de détails
sur les différentes fonctions présentées ici.
3-1. Le format "Raw"▲
Ce que nous appelons "Raw" est en fait le format qui correspond à la donnée
brute, telle qu'elle a été saisie par l'utilisateur, sans aucune
transformation.
Exemple : a'a"a<a>a</a>a\a
3-2. Le format "Échappé" (Escape)▲
Il s'agit ici d'ajouter un backslash (\) devant certains métacaractères.
La méthode à appeler dépend de l'utilisation souhaitée.
Méthodes en PHP :
addslashes,
mysql_real_escape_string,
pg_escape_string,
escapeshellcmd,
escapeshellarg,
quotemeta,
etc.
Exemple : a\'a\"a<a>a</a>a\\a
3-3. Le format "HTML"▲
Il s'agit ici de transformer certains caractères spéciaux pour qu'ils
s'affichent correctement dans une page quel que soit le jeu de caractères utilisé
et qu'ils ne soient pas considérés comme des balises HTML.
Méthodes en PHP :
htmlentities,
htmlspecialchars.
Exemple : a'a"a<a>a</a>a\a
3-4. Le format "URL"▲
Il s'agit ici de transformer certains caractères spéciaux pour qu'ils puissent
être utilisés dans une URL.
Méthodes en PHP :
urlencode,
rawurlencode.
Exemple : a'a%22a%3Ca%3Ea%3C%2Fa%3Ea%5Ca
3-5. Le format "base64"▲
Il s'agit ici de transformer une chaîne en une suite de caractères simples, sans
accents ou signes spéciaux, pour qu'ils puissent être envoyés sur le réseau ou
à une autre application (par exemple Flash).
Méthodes en PHP :
base64_encode.
Exemple : YSdhImE8YT5hPC9hPmFcYQ==
4. Le formatage automatique - magic_quotes▲
La plupart du temps les données resteront dans leur format d'origine lors de
leur manipulation.
Cependant, il y a une exception.
Il s'agit de trois vieilles options de PHP, donc en principe vous ne devriez
plus avoir de problèmes avec :
- magic_quotes_gpc ;
- magic_quotes_sybase ;
- magic_quotes_runtime.
Elles ont toutes été marquées
DEPRECATED
en PHP 5.3.0.
Les deux premières options, lorsqu'elles étaient activées, modifiaient
automatiquement les variables lues depuis les formulaires GET et POST ainsi que
les données venant de COOKIE, en appliquant la méthode "addslashes" dessus.
L'option magic_quotes_runtime modifiait quant à elle le retour de nombreuses
fonctions de lecture de données, dont : file, gets, exec, mysqli_fetch_*
(mais pas mysql_fetch_* !!).
PHP a fini par revenir sur ces options pour plusieurs raisons :
- Performance : toutes les données étaient échappées, alors que toutes les
données n'en avaient pas besoin ;
- Lourdeur du code : lorsque le remplacement n'était pas souhaité, il fallait
appeler explicitement stripslashes() à la lecture de la variable ;
- Sécurité : l'ajout des backslash () par la méthode addslashes avait pour but
de protéger les requêtes des injections SQL, mais cela ne fonctionnait pas de
toute façon car la méthode addslashes ne gérait pas correctement les caractères
multi-octets.
De plus, dans la mesure où il s'agissait d'options de configuration, un site web
pouvait très bien fonctionner correctement sur un serveur et pas du tout sur un
autre avec une configuration différente.
5. La destination des données▲
Pour chaque cas, on donnera un exemple de donnée formatée ainsi que le risque encouru si le formatage n'est pas effectué.
5-1. L'affichage sur la page▲
L'affichage d'une donnée sur une page HTML doit se faire en échappant les
entités HTML à l'aide de
htmlentities
(ou
htmlspecialchars
) :
- htmlentities
convertira également les accents en entité HTML, ce qui vous assurera une
meilleure compatibilité de vos pages web ;
- htmlspecialchars
se contentera de convertir les métacaractères HTML.
Il est important pour des raisons de sécurité de spécifier le bon encodage à l'aide du paramètre optionnel "charset".
Si vous êtes amenés à utiliser les fonctions inverses, html_entity_decode ou htmlspecialchars_decode, c'est que vous avez mal conçu votre application.
Donnée brute : a<u>a</u>a
Donnée formatée : a<u>a</u>a
Risque : faille de sécurité de type
Cross Site Scripting
(XSS).
Lien vers la documentation PHP
(Encodage d'une valeur d'un formulaire ou d'une URL).
5-2. L'affichage dans une balise HTML▲
Vous devez ici protéger essentiellement les caractères de séparation
utilisés dans la balise HTML, c'est-à-dire les guillemets (") ou les quotes ('),
mais aussi les autres caractères spéciaux pour un affichage correct.
Vous pouvez donc utiliser
htmlspecialchars
ou htmlentities.
Il est important pour des raisons de sécurité de spécifier le bon encodage à l'aide du paramètre optionnel "charset".
Si vos balises HTML utilisent des quotes (') et non des guillemets (") il faudra utiliser l'option ENT_QUOTES.
Donnée brute : a'a"a
Donnée formatée : a'a"a
Risque : faille de sécurité de type
Cross Site Scripting
(XSS).
Lien vers la documentation PHP
(Encodage d'une valeur d'un formulaire ou d'une URL).
5-3. L'utilisation dans une URL▲
Une variable qui est utilisée pour construire une URL doit être encodée avec
urlencode
ou
rawurlencode.
Il est conseillé d'utiliser rawurlencode plutôt que urlencode. En effet,
urlencode utilise l'ancienne norme de formatage des URL et non la norme
actuelle, respectée par rawurlencode depuis PHP 5.3.0, qui est la
RFC3986.
La différence la plus courante entre les deux méthodes est que urlencode va
remplacer les espaces dans l'URL par le signe "+" alors que rawurlencode va les
remplacer par la chaîne "%20".
Attention, seules les données doivent être encodées avec rawurlencode ! Les
caractères de séparation ?, = et & ne doivent pas être encodés.
Il est possible d'encoder le nom de la page ou le nom de la variable s'ils
contiennent un ou plusieurs caractères spéciaux.
Donnée brute : a"a
Donnée formatée : a%22a
Risque : faille de sécurité de type
Cross Site Scripting
(XSS).
Lien vers la documentation PHP
(Encodage d'une valeur d'un formulaire ou d'une URL).
5-4. L'utilisation dans du code JavaScript▲
Lorsque vous avez une variable PHP qui sert à construire du code JavaScript, il
est conseillé d'utiliser des guillemets pour entourer la variable PHP et
d'utiliser
addslashes.
Donnée brute : a'a"a
Donnée formatée : a\'a\"a
Risque : faille de sécurité de type
Cross Site Scripting
(XSS).
Lien vers la documentation PHP
(Passage de variable de JavaScript à PHP).
5-5. L'utilisation dans une requête SQL▲
Pour échapper correctement une donnée destinée à être utilisée dans une requête
SQL il faut absolument utiliser les méthodes fournies par la base de données
utilisée :
- mysql_real_escape_string
pour les bases MySQL ;
- mysqli_real_escape_string
pour les connexions mysqli ;
- pg_escape_string
pour les bases PostgreSQL.
Donnée brute : a'a"a
Donnée formatée : jamais destinée à être affichée, dépend de la base de données
Risque : faille de sécurité de type
Injection SQL.
5-6. Le stockage dans une base de données▲
Comme expliqué précédemment, le stockage doit se faire au format de saisie.
Cependant, pour inclure la donnée dans une requête, on doit utiliser les mêmes
méthodes que pour construire une requête ().
Donnée brute : a'a"a
Donnée formatée : jamais destinée à être affichée, dépend de la base de données
Risque : faille de sécurité de type
Injection SQL.
5-7. L'utilisation dans un mail au format HTML▲
Souvent oublié lors des tests, lorsqu'on génère le corps d'un mail au format
HTML, le contenu ne doit pas être protégé par htmlspecialchars ou htmlentities.
Ici on souhaite en général que le code HTML soit interprété.
Donnée brute : a<u>a</u>a
Attention tout de même que le contenu du mail ne vienne pas d'un utilisateur.
Sinon il est plus prudent de protéger le contenu non sécurisé avec
htmlspecialchars
ou
htmlentities.
Risque : faille de sécurité de type
Cross Site Scripting
(XSS).
5-8. L'utilisation dans un appel système▲
Peu utilisé, mais très dangereux si on l'utilise et qu'on l'oublie !
Les paramètres d'appels des méthodes suivantes doivent être formatés :
- exec ;
- system ;
- passthru ;
- proc_open ;
- shell_exec ;
- ` ` (guillemets obliques, ou backquote : Alt Gr+7).
Ici on doit utiliser les méthodes
escapeshellarg
et
escapeshellcmd.
Donnée brute : a#a$a;a
Donnée formatée : a\#a\$a\;a
Risque : faille de sécurité permettant de lancer n'importe quelle
commande sur le serveur...
5-9. L'utilisation dans une expression régulière▲
Là encore, l'utilisation est rare.
Si vous construisez dynamiquement une expression régulière avec des données
venant de l'utilisateur, il peut être intéressant de les protéger pour que
votre expression fonctionne.
Pour cela, on utilise
preg_quote
ou
quotemeta.
Donnée brute : a.a$a*a
Donnée formatée : a\.a\$a\*a
Risque : code non fonctionnel et/ou faille applicative.
5-10. Encodage pour transfert ou pièce jointe▲
Lorsque vous avez besoin de transférer une donnée à une autre application (Flash
par exemple) ou d'ajouter une pièce jointe à un mail envoyé en PHP, vous pouvez
utiliser le format base64 avec
base64_encode. Il s'agit d'un format qui transforme complètement la chaîne en entrée mais qui
assure que la chaîne encodée ne contient que des caractères simples (et donc
qu'on n'aura pas de problème d'encodage, quel que soit le charset utilisé).
Pour décoder la chaîne on utilisera la méthode
base64_decode.
Donnée brute : a'a"a<a>a</a>a\a
Donnée formatée : YSdhImE8YT5hPC9hPmFcYQ==
Risque : code non fonctionnel et/ou faille applicative.
6. Solutions▲
6-1. Manuellement, au cas par cas▲
C'est le cas de la plupart des sites web. Le formatage de la donnée est
totalement oublié et on le rajoute uniquement lorsqu'on détecte un problème (un
utilisateur s'inscrit avec une apostrophe dans le login par exemple).
Inutile de dire que ce genre de site n'est pas sécurisé.
6-2. Interdire au lieu de gérer▲
La solution la plus simple, c'est d'interdire tous les caractères pouvant poser
problème.
Maintenant, ce n'est pas forcément souhaitable d'avoir un site Web sans aucune
apostrophe et autre métacaractère. Certains sites le gèrent en remplaçant les
apostrophes par des underscores (_) mais ce n'est pas très propre quand même...
6-3. Tout échapper▲
Une autre solution "simple" :
Elle consiste à ne pas réfléchir et à filtrer toutes les variables sans
exception, tout le temps, avec toutes les méthodes possibles.
Il est par contre probable que l'utilisateur voit quelque chose comme ça :
Donnée saisie : a'a"<u>a</u>a
Donnée affichée : a\\\'a\\\"a&amp;lt;u&amp;gt;a&amp;lt;/u&amp;gt;a
Évidemment, ce n'est pas très joli... mais je tenais à en parler parce que j'ai
déjà reçu des mails de sites parlant de sécurité informatique avec trois
antislashs devant un guillemet.
Personnellement, quand je vois ça, je ne me dis pas que le site est sécurisé,
mais que le webmaster manque de compétence et de confiance et ne sait pas
réellement ce qu'il fait.
6-4. Cas particulier de la base de données : PDO▲
Il existe une autre solution qui permet d'éviter (presque) tous les problèmes
d'injection SQL : passer par une abstraction de base de données comme PDO et
utiliser les requêtes préparées.
Lorsque vous utilisez une requête préparée et que vous lui passez des
paramètres, vous passez les paramètres et la requête séparément à la base de
données. Il est donc impossible pour celle-ci de les "confondre" avec la
requête elle-même, quel que soit leur contenu.
Cela permet d'éviter 99.9 % des failles de type Injection SQL.
Documentation PHP sur PDO
Pour ceux qui veulent savoir ce qu'est le 0.01 % restant (Slide 31)
6-5. L'extension Filter (>= PHP 5.2.0)▲
Depuis la version 5.2.0, PHP nous fournit l'extension Filter (et activée par
défaut !).
La documentation est assez bien faite :
http://php.net/manual/fr/book.filter.php
Filter permet de transformer une donnée dans le format URL, échappement de
quotes (') et de guillemets(").
Cependant, l'utilisateur a la possibilité d'étendre Filter avec ses propres
méthodes de formatage.
À noter la possibilité de valider toutes les données d'un formulaire en une
seule fois, ce qui est assez propre et lisible (exemple sur la page de la
méthode
filter_input_array).
Filter permet également de :
- supprimer certains caractères d'une chaîne ;
- vérifier le format d'une chaîne (alphanumérique, mail, IP, etc.).
Il existe aussi une traduction d'un article sur Filter sur developpez.com.
6-6. In.class (PHP 5.3.0)▲
Les solutions précédentes ne me convenant pas, j'ai développé une classe qui
permet de formater facilement les données.
Mon cahier des charges était le suivant :
- facile d'utilisation => l'API est intuitive ;
- concis => API réduite au minimum avec l'appel dynamique sur les objets ;
- gérant les magic_quotes => la première version datait de 2007. C'est inutile
maintenant mais je ne l'ai pas supprimée ;
- performant => les données formatées sont mises en cache, donc une même
variable ne sera jamais formatée deux fois dans la même page.
Je diffuse maintenant la dernière version du code, après l'avoir réécrit avec
les nouveautés de PHP 5.3.0 (Late Static Binding notamment).
Exemples d'utilisation :
echo htmlentities($_GET
[
'
variable
'
],
ENT_QUOTES);
echo mysql_real_escape_string($maVariableLocale
);
deviennent
$In
=
In::
getInstance();
// en début de page ou de méthode
echo $In
->
G->
HTML->
variable;
echo $In
->
SQL($maVariableLocale
);
Plus d'exemples et d'explications dans l'en-tête de la classe disponible ici :
6-7. Résumé▲
Le tableau ci-dessous résume les avantages / inconvénients de chaque solution :
Solution | Interdire | Tout échapper | Filter | In.class |
---|---|---|---|---|
Généralités | ||||
Version de PHP minimum | Toutes | Toutes | 5.2.0 | 5.3.0 |
Code propre | Non | Non | Oui | Oui |
Code concis | Non | Non | Non | Oui |
Natif PHP | Oui | Oui | Oui | Non |
Extensible | N/A | N/A | Oui | Oui |
Gestion des magic_quotes | Non | Non | Non | Oui |
Formatage multiple | Non | Non | Oui | Non |
Formats supportés nativement | ||||
Raw | Oui | Oui | Oui | Oui |
Html | N/A | N/A | Non | Oui |
URL | N/A | N/A | Oui | Oui |
Base64 | N/A | N/A | Non | Oui |
Échappement ' | N/A | N/A | Oui | Oui |
Échappement " | N/A | N/A | Oui | Oui |
Échappement SQL | N/A | N/A | Non | Oui |
Appel système | N/A | N/A | Non | Non |
Expression régulière | N/A | N/A | Non | Non |
7. Tests▲
7-1. Chaîne de test▲
Lorsque vous voulez tester un formulaire, une URL ou une variable contre tout ce qui a été expliqué au-dessus, vous pouvez utiliser la chaîne de caractères suivante :
a'a"a<u>a</u>
a/a\a$a;a`a#a
Si le comportement est anormal, ou que la chaîne est modifiée à l'affichage,
c'est que les données sont mal gérées.
Il est très rare que les sites gèrent l'intégralité de ces caractères, surtout
dans les champs "login/password" d'un site.
Le champ password est le plus délicat, car s'il est transformé avec un hash MD5
ou autre, il convient de vérifier qu'il subit les mêmes transformations lors de
la saisie et lors de la vérification, avant la transformation MD5.
Remerciements▲
Merci à Mahefasoa,
jacques_jean
et ClaudeLELOUP
pour la relecture de l'article !
Merci également à Yogui
pour ses remarques pertinentes.