PHP

Dans cette partie nous allons nous intéresser à la sécurisation programmatique de l’upload de fichiers en PHP. Pour cela, nous allons nous appuyer sur ce que PHP7.4 a à nous offrir.

Écrasement de fichiers

Le premier problème dans l’upload de fichiers est la duplication des noms de fichiers. En effet, un fichier upload qui aurait le même nom qu’un fichier déjà existant l’écraserait. Avec cette méthode, on peut facilement imaginer qu’un attaquant écrase les fichiers exécutés par le serveur web pour effectuer toute sorte d’actions malveillantes. Il est donc impératif de réaliser ce renommage.

Plusieurs solutions s’offrent à nous pour celui-ci:

  • Hash
  • Epoch Time
  • UID
  • Incrément
  • Random value

Le principe va être de réaliser un salage sur le nom de fichier originel: fichier_<salage>.html. De cette façon on protège les fichiers du serveur et les fichier de base du système “.” et “..” qui n’auront pas d’agrément à leur nom quelle que soit la méthode. Mais il faut également protéger les fichiers uploadés par les utilisateurs eux-mêmes.

Toutes les méthodes citées au-dessus sont valides, mais pas toutes adaptées à notre cas d’usage ni à notre environnement. En effet, PHP7.4 n’offre pas de fonction capables de générer des UID dont la l’unicité est garantie. De la même façon, si le salage était une valeur aléatoire, il existerait une possibilité de duplication de fichiers avec un grand nombre de tentatives, ce qui constitue une faille importante. L’UID aurait été la solution la plus adaptée mais nous allons devoir nous en passer ici.

Le valeur incrémentée n’est également pas une solution. Bien que celle-ci garantisse un salage unique pour chaque fichier, celle-ci donnerait une information sur le nombre de fichiers uploadés de puis le démarrage du serveur. Ce n’est pas nécessairement une donnée que l’on souhaite faire apparaître. De plus, en cas de redémarrage du serveur web, le compteur recommencerait à 1 et ce serait alors l’occasion pour un attaquant de réécrire les fichiers uploadés avant le redémarrage.

Nous avons donc fait le choix d’un salage résultant de la combinaison de l’epoch time (en millisecondes) et d’un hash du fichier avec l’algorithm sha256: fichier_<epochtime>_<hash>.html. Le fichier est donc rendu unique grâce à une composante temporelle et relative a son contenu. Ainsi, le risque d’écrasement de fichier est considérablement réduit.

[...]

#########################################################################################
# On renomme le fichier de façon a éviter les écrasement de fichier lors des uploads
#########################################################################################

function build_filename($basename_without_extension, $extension) {
        $salt_epochtime = microtime(true) * 10000; # Epoch time en millisecondes
        $res = $basename_without_extension . "_" . $salt_epochtime;

        $salt_hash = hash_file('sha256', $_FILES['userfile']['tmp_name']);
        $res = $res . "_" . $salt_hash;
                
        if (!empty($extension)) {
                $res = $res . "." . "$extension";
        }
        return $res;
}

$new_filename = build_filename($basename_without_extension, $extension); 

[...]

Extensions frauduleuses

Afin de vérifier que le type de fichier uploadé corresponde bien à l’extension de celui-ci, nous allons vérifier que le type MIME correspond à l’extension fournie. Ceci permet d’éviter que des fichiers soient dissimulés derrière des extensions pouvant déjouer la vigilance des autres couches de sécurité.

L’idée principale est d’extraire l’extension du fichier uploadé pour en déduire tous les types MIME possibles. On récupère ensuite le type MIME effectif du fichier et nous regardons si l’un des types MIME possible correspond au type MIME effectif. Si c’est le cas, le fichier est validé et continue le processus d’upload. Sinon il est rejeté. Il existe un cas particulier, celui des executables linux qui ne possède généralement pas d’extension. Le type MIME effectif du fichier doit donc être “application/x-sharedlib”.

Pour réaliser cette tâche, PHP7.4 nous offre un fonction capable d’extraire le type MIME d’un fichier. En revanche, aucun moyen de récupérer les type MIME possibles à partir d’une extension de fichier. Il a donc fallu importer un bibliothèque open-source spécialisée dans cette tâche dont nous allons détailler l’installation:

Pour cette partie, nous utiliserons une dépendance disponible à: https://github.com/ralouphie/mimey. Il s’agit d’une ressource libre de droits.

franck@INFRA01:~/Bureau/TEST_FILES$ sudo apt update

DM7

Installer les paquets suivants:

franck@INFRA01:~/Bureau/TEST_FILES$ sudo apt install wget php-cli php-zip unzip

DM7

Récupérer les fichiers suivants:

franck@INFRA01:~/Bureau/TEST_FILES$ php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"

DM7

Vérifier que les fichiers téléchargés sont intègres:

franck@INFRA01:~/Bureau/TEST_FILES$ php -r "if (hash_file('SHA384', 'composer-setup.php') === '$HASH') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;"

DM7

Lancer l’installer afin d’installer composer:

franck@INFRA01:~/Bureau/TEST_FILES$ sudo php composer-setup.php --install-dir=/usr/local/bin --filename=composer

DM7

Vérifier que composer est effectivement installé:

franck@INFRA01:~/Bureau/TEST_FILES$ composer

DM7

Installer maintenant la dépendance ralouphie/mimey souhaitée précédemment sans être root de préférence:

franck@INFRA01:~/Bureau/TEST_FILES$ composer require ralouphie/mimey

DM7

Copier maintenant dans le dossier content le code php du site web:

  • /vendor/ (dossier)
  • composer.json
  • composer.lock

Créer un nouveau dossier qui contiendra toutes les dépendances:

root@INFRA01:/var/www/html/php_legit# mkdir project

Entrer dans le nouveau dossier:

root@INFRA01:/var/www/html/php_legit# cd project/

Copier récursivement le dossier vendor/ précédemment généré:

root@INFRA01:/var/www/html/php_legit/project# cp -r /home/franck/Bureau/TEST_FILES/vendor/ .

DM7

Copier les fichiers composer.* précédemment générés:

root@INFRA01:/var/www/html/php_legit/project# cp -r /home/franck/Bureau/TEST_FILES/composer.* .

DM7

Voici l’architecture résultante des dépendance composer:

root@INFRA01:/var/www/html/php_legit# tree --du -h .
.
├── [285K]  project
│   ├── [  61]  composer.json
│   ├── [  61]  composer.lock
│   └── [281K]  vendor
│       ├── [ 427]  autoload.php
│       ├── [ 42K]  composer
│       │   ├── [ 222]  autoload_classmap.php
│       │   ├── [ 139]  autoload_namespaces.php
│       │   ├── [ 194]  autoload_psr4.php
│       │   ├── [1.1K]  autoload_real.php
│       │   ├── [1.0K]  autoload_static.php
│       │   ├── [ 16K]  ClassLoader.php
│       │   ├── [1.8K]  installed.json
│       │   ├── [1.0K]  installed.php
│       │   ├── [ 15K]  InstalledVersions.php
│       │   ├── [1.0K]  LICENSE
│       │   └── [ 925]  platform_check.php
│       └── [235K]  ralouphie
│           └── [231K]  mimey
│               ├── [4.5K]  bin
│               │   └── [ 466]  generate.php
│               ├── [ 554]  composer.json
│               ├── [1.1K]  license
│               ├── [ 59K]  mime.types
│               ├── [ 586]  mime.types.custom
│               ├── [129K]  mime.types.php
│               ├── [ 422]  phpunit.xml
│               ├── [3.4K]  readme.md
│               ├── [ 13K]  src
│               │   ├── [3.0K]  MimeMappingBuilder.php
│               │   ├── [1.7K]  MimeMappingGenerator.php
│               │   ├── [1.2K]  MimeTypesInterface.php
│               │   └── [2.6K]  MimeTypes.php
│               └── [ 15K]  tests
│                   ├── [ 190]  bootstrap.php
│                   └── [ 11K]  src
│                       ├── [2.9K]  MimeMappingBuilderTest.php
│                       ├── [1.0K]  MimeMappingGeneratorTest.php
│                       └── [2.8K]  MimeTypesTest.php
└── [3.8K]  test.php

Il faut maintenant lier la dépendance composer avec le code php du site web:

<?php

	# Import de la dépendance
	require __DIR__ . '/project/vendor/autoload.php';
	use Mimey\MimeTypes;

        [...]

On peut ensuite utiliser la bibliothèque directement dans le code php.

Voici le code correspondant au process décrit plus haut:

[...]

#########################################################################################
# On vérifie la validité du nom du fichier et de son extension
#########################################################################################

$basename_without_extension = pathinfo($basename, PATHINFO_FILENAME);
$extension = pathinfo($basename, PATHINFO_EXTENSION);

# Un fichier sans extension est valide, mais un fichier dont le nom complet est vide ne l'est pas
if (empty($extension) && empty($basename_without_extension)) {
        upload_failure();
}

$mime_type = mime_content_type($_FILES['userfile']['tmp_name']);

# Si pas d'extension, le type MIME DOIT être "application/x-sharedlib"
if (empty($extension)) {
        if ((strcmp($mime_type, "application/x-sharedlib") != 0)) {
                upload_failure();
        }
}
else {
        # S'il y a une extension, on récupère tous les types MIME 
        $mimes = new \Mimey\MimeTypes;	
        $mime_types = $mimes->getAllMimeTypes($extension);

        # On vérifie la correspondance entre le type MIME effectif du fichier et
        # l'un des types MIME possibles selon son extension
        if (!in_array($mime_type, $mime_types)) {
                upload_failure();
        }
}


[...]

Tous les fichiers dont l’extension ne correspond pas au type MIME effectif du fichier sont à-présent rejetés.

Éviter le Path traversal

Éviter le path traversal permet d’éviter que par un fichier avec un nom de fichier incongru du type ./../../test.txt représentant un chemin relatif ou “/usr/bin/ls” l’attaquant puisse atteindre des zones du système auquel il n’aurait pas dû avoir accès.

Il s’agit donc de récupérer uniquement dans le nom de fichier, dépourvu de tout path additionnel.

$uploadname = $_FILES['userfile']['name'];

# Evite le Path traversal, attention toutefois les path à la
# Windows (.\..\test.php) ne sont pas pris en compte
$basename = basename($uploadname); 

Interdire les fichier avec plusieurs ‘.’

Afin d’éviter la dissimulation de l’extension réelle ou malveillante du fichier, qui pourrait provoquer un dysfonctionnement des mécanismes de validation présentés au-dessus, nous avons fait le choix de limiter le nombre de caractères ‘.’ à un 1 ou 0. Excluant d’office les fichiers avec plusieurs extensions, cachés avec extension, ou contenant “..” car ces derniers sont considérés comme malveillants par nature.

#########################################################################################
# On verifie qu'il n'y ait pas plus de 1 '.' dans le nom du fichier uploadé
#########################################################################################

if (substr_count($uploadname, ".") > 1) {
        upload_failure();
}

Ne pas trop en dire

Lorsqu’une erreur survient, il est préférable de rester vague sur le message d’erreur renvoyé à l’utilisateur. Par exemple, lorsque sur un site quelconque on manque notre authentification, les sites nous indiquent très rarement quelle a été la cause de l’erreur (mot de passe incorrecte ou login incorrect) et nous envoie un message plutôt générique du type: login ou mot de passe incorrecte.

C’est également un problème bien connu du protocole ICMP qui peut parfois être trop verbeux sur les erreurs qu’il rapport.

C’est dans cette perspective que nous avons limité les messages d’erreur à un simple texte: “failure

[...]

function upload_failure() {
        echo "failure\n";
        exit();
}

[...]

Bonus : recaptcha

Un autre mécanisme que nous aurions aimé ajouter est le recaptcha. Celui-ci demande à l’utilisateur de prouver qu’il n’est pas une machine. Cela aurait permis d’éviter l’utilisation du site par des automates (fuzzer notamment). Malheureusement, n’ayant pas le droit d’utiliser d’outils propriétaires (ici une bibliothèque de Google Inc.) nous n’avons pas pu en faire usage.

L’implémentation de captcha “maison” aurait pu être faite, mais la complexité d’un tel système n’est pas à notre portée sur le plan qualitatif.

Voici le fichier complet:

test.php