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 ( &lt; ) 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&#39;a&quot;a&lt;a&gt;a&lt;/a&gt;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&lt;u&gt;a&lt;/u&gt;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&#039;a&quot;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;amp;lt;u&amp;amp;gt;a&amp;amp;lt;/u&amp;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 :

 
Sélectionnez
echo htmlentities($_GET['variable'], ENT_QUOTES);
echo mysql_real_escape_string($maVariableLocale);

deviennent

 
Sélectionnez
$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 :

Voir/Télécharger In.class.php

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 :

 
Sélectionnez
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.