Mis à jour le mercredi 8 mars 2017
  • 4 heures
  • Facile

La faille upload

Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

 

Explication

La faille upload est une des failles les plus dangereuses. Vous connaissez sûrement la balise HTML qui permet l'upload de fichier :

<input type="file" />

Cette balise est utilisée sur de nombreux sites qui proposent à leurs utilisateurs d'avoir une photo de profil ou d'inclure une image dans un message sur le forum. Le problème, c'est que la balise upload permet d'uploader n'importe quel fichier. Un utilisateur malintentionné pourrait sans problème s'amuser à uploader un fichier PHP malveillant (web shell par exemple) qui lui permettrait de prendre le contrôle total de votre site, et même par rebond, de prendre la main sur le serveur en backoffice ! Et même si vous avez fait les quelques vérifications réglementaires, il est possible qu'il y ait malgré tout des faiblesses exploitables.

La double extension

Vous avez peut-être pensé à limiter l'upload à certains types de fichier, et c'est tout à votre honneur. Imaginons un site qui autorise uniquement les fichiers .jpg. Qu'est-ce qui m'empêche de renommer mon fichier en "backdoor.php.jpg" et de l'envoyer ? Eh bien c'est là tout le problème : rien ! Il est également possible de renommer son fichier en tant que "backdoor.php\0.jpg". Le "\0" indique au serveur qu'il arrive en bout de chaine. Tout ce qui suit ne sera donc pas interprété. Le fichier sera enregistré en tant que "backdoor.php" et aura passé haut la main le test d'extension.

Le content-type

Cette technique consiste à utiliser une extension sur le navigateur pour altérer les données envoyées au serveur. Il sera alors enfantin de faire croire au serveur que notre fichier est un fichier image et non un fichier PHP.

Je n'en dirai pas plus sur cette faille, car je ne suis pas ici pour faire de vous des hackers en herbe. Je pense que les informations données précédemment sont amplement suffisantes pour comprendre la gravité de cette faille et se décider à s'en protéger.

Comment s'en protéger

Bon tout d'abord, sachez qu'il est très difficile de créer un filtre complètement fiable. Mais nous allons tout de même créer un fichier bien solide, auquel même hacker vaillant n'oserait pas s'attaquer. :soleil:

Tout d'abord, j'aimerai porter à votre attention cet article (un peu vieux, certes) qui résume parfaitement les grands points à considérer pour mettre en place un script d'upload sécurisé. Pour ceux qui ne seraient pas familiers avec la langue de Shakespeare, voilà un petit résumé :

Les 8 règles basiques pour implémenter un système d'upload sécurisé

  1. Renommez le fichier

  2. N'enregistrez pas vos fichiers à la racine de votre site

  3. Vérifiez la taille du fichier

  4. Ne vous fiez pas aux extensions

  5. Effectuez un scan anti-malware

  6. Gardez le contrôle des permissions (CHMOD)

  7. N’autorisez l'upload qu'aux utilisateurs inscrits et authentifiés

  8. Limitez le nombre de fichiers qu'un utilisateurs peux mettre en ligne

Vous ne m'en voudrez pas, je l'ai un peu traduis à ma sauce pour ne pas faire du franglais ^^

Attaquons nous donc à ce script ! Ne vous inquiétez pas, on va y aller pas à pas.

Tout d'abord, il nous faut renommer le fichier en question. Un hash généré (pseudo) aléatoirement fera particulièrement bien l'affaire :

<?php

$file = $_FILES["MY_FILE"];
$actualName = $file['tmp_name'];
$newName = bin2hex(mcrypt_create_iv(32, MCRYPT_DEV_URANDOM));

?>

Il nous faut maintenant choisir un répertoire d'upload cohérent avec notre situation. Ce sera donc à vous de déterminer où vous souhaitez que le fichiers atterrissent. Il faudra penser à bien définir les droits lecture/écriture, ainsi que mettre en place un solide htaccess qui interdirait de voir l’index of du répertoire en question. Dans mon cas, je me contenterais d'un répertoire "upload" qui convient parfaitement à un cas général.

 

<?php

$file = $_FILES["MY_FILE"];
$actualName = $file['tmp_name'];
$newName = bin2hex(mcrypt_create_iv(32, MCRYPT_DEV_URANDOM));
$path = "/upload"

?>

On va ensuite vérifier l'extension proposée, et voir si elle nous convient. Si tout va bien, il nous suiffera de la rajouter après notre nouveau nom de fichier.

<?php

$file = $_FILES["MY_FILE"];
$actualName = $file['tmp_name'];
$newName = bin2hex(mcrypt_create_iv(32, MCRYPT_DEV_URANDOM));
$path = "/upload"

// On crée un tableau avec les extensions autorisées
$legalExtensions = array("JPG", "PNG", "GIF", "TXT");

// On récupère l'extension du fichier soumis et on vérifie qu'elle soit dans notre tableau
$extension = pathinfo($file['MY_FILE'], PATHINFO_EXTENSION);

if (in_array($extension, $legalExtensions)) {
    move_uploaded_file($actualName, $path.'/'.$newName.'.'.$extension);
}

?>

On a déjà un bon début ! On va ajouter quelques vérifications d'usage pour mettre un peu tout ça en forme :

<?php

// Varibale d'erreur par soucis de lisibilité
// Evite d'imbriquer trop de if/else, on pourrait aisément s'en passer
$error = false;

// On définis nos constantes
$newName = bin2hex(mcrypt_create_iv(32, MCRYPT_DEV_URANDOM));
$path = "/upload"
$legalExtensions = array("JPG", "PNG", "GIF", "TXT");
$legalSize = "10000000" // 10000000 Octets = 10 MO

// On récupères les infos
$file = $_FILES["MY_FILE"];
$actualName = $file['tmp_name'];
$actualSize = $file['size'];
$extension = pathinfo($file['MY_FILE'], PATHINFO_EXTENSION);

// On s'assure que le fichier n'est pas vide
if ($actualName == 0 || $actualSize == 0) {
    $error = true;
}

// On vérifie qu'un fichier portant le même nom n'est pas présent sur le serveur
if (file_exists($path.'/'.$newName.'.'.$extension)) {
    $error = true;
}

// On effectue nos vérifications réglementaires
if (!$error) {
    if ($actualSize < $legalSize) {
        if (in_array($extension, $legalExtensions)) {
            move_uploaded_file($actualName, $path.'/'.$newName.'.'.$extension);
        }
    }
}

else {
    
    // On supprime le fichier du serveur
    @unlink($path.'/'.$newName.'.'.$extension);
    
    echo "Une erreur s'est produite";
    
}

?>

Voilà ça me semble plutôt bien tout ça, vous en pensez quoi ? ;) 

S'il s'agit d'un image ou d'un fichier audio (par exemple), on pourrait se permettre d'ouvrir le fichier et de vérifier qu'il ne contient pas de caractère suspicieux. En effet, la découverte d'un chevron ou d'un point virgule laisserait à penser que l'on a affaire à du code. Et comme vous l’aurez surement compris, ce n'est pas dutout une situation d'avenir. On pourrait palier à ce problème de la sorte :

<?php

// [...]

$handle = fopen($nom, 'r');

if ($handle) {

    while (!feof($handle) AND $erreur == 0) {

        $buffer = fgets($handle);

        switch (true) {
            case strstr($buffer,'<'):
            $error = true;
            break;

            case strstr($buffer,'>'):
            $erreur += 1;
             break;

            case strstr($buffer,';'):
            $erreur += 1;
            break;

            case strstr($buffer,'&'):
            $erreur += 1;
             break;

            case strstr($buffer,'?'):
            $erreur += 1;
            break;
        }
    }

fclose($handle);

// [...]

?>

L'idéal serait de coupler tout ça à un solide htaccess.

On pourrait imaginer quelque chose comme ceci :

deny from all

<Files ~ “^w+.(gif|jpg|png|txt)$”>
order deny,allow
allow from all
</Files>

Voilà un petit trou en plus de rebouché. :D

Exemple de certificat de réussite
Exemple de certificat de réussite