web-dev-qa-db-fra.com

Script de téléchargement d'image entièrement sécurisé

Je ne sais pas si cela va arriver, mais je vais l'essayer.

Pendant une heure, j'ai fait des recherches sur la sécurité du téléchargement d'images. J'ai appris qu'il y avait beaucoup de fonctions pour tester le téléchargement.

Dans mon projet, je dois être en sécurité avec les images téléchargées. Il peut également y en avoir une très grande quantité et nécessiter beaucoup de bande passante. L'achat d'une API n'est donc pas une option.

Alors j’ai décidé d’obtenir un PHP script complet pour un téléchargement d’images VRAIMENT sécurisé). Je pense aussi que cela aidera beaucoup de gens, car il est impossible de le trouver réellement sécurisé. Mais je ne suis pas expert en php, il est donc très difficile pour moi d’ajouter des fonctions, je vais donc demander à cette aide de la communauté de créer un script complet pour le téléchargement réellement sécurisé des images.

De très bons sujets à ce sujet sont ici (cependant, ils ne font que dire ce qu'il faut faire, mais comment le faire, et comme je l'ai dit, je ne suis pas un expert en PHP, je ne peux donc pas tout faire. par moi-même): liste de contrôle de sécurité de téléchargement d'images PHPhttps://security.stackexchange.com/questions/32852/risks-of-a-php-image-upload-form =

En résumé, ils disent que c'est ce qui est nécessaire pour le téléchargement d'images de sécurité (je citerai les pages ci-dessus):

  • Désactivez PHP à partir du dossier de téléchargement en utilisant .httaccess.
  • Ne pas autoriser le téléchargement si le nom du fichier contient la chaîne "php".
  • Autoriser uniquement les extensions: jpg, jpeg, gif et png.
  • Autoriser uniquement le type de fichier image.
  • Interdit les images avec deux types de fichiers.
  • Changer le nom de l'image. Télécharger dans un sous-répertoire, pas le répertoire racine.

Aussi:

  • Retouchez l'image avec Gd (ou Imagick) et enregistrez l'image traitée. Tous les autres sont juste amusants et ennuyeux pour les pirates "
  • Comme indiqué par rr, utilisez move_uploaded_file () pour tout envoi "
  • En passant, vous voudriez être très restrictif à propos de votre dossier de téléchargement. Ces endroits sont l’un des coins sombres où de nombreux exploits
    se produire. Ceci est valable pour tout type de téléchargement et toute programmation
    langue/serveur. Vérifier
    https://www.owasp.org/index.php/Unrestricted_File_Upload
  • Niveau 1: Vérifier l'extension (le fichier d'extension se termine par)
  • Niveau 2: Vérifiez le type MIME ($ file_info = getimagesize ($ _ FILES ['image_file']; $ file_mime = $ file_info ['mime'];)
  • Niveau 3: lit les 100 premiers octets et vérifie s’ils en ont dans la plage suivante: ASCII 0-8, 12-31 (décimal).
  • Niveau 4: Recherchez des nombres magiques dans l'en-tête (10 à 20 premiers octets du fichier). Vous pouvez trouver ici quelques octets d’en-tête de fichiers:
    http://en.wikipedia.org/wiki/Magic_number_%28programming%29#Examples
  • Vous voudrez peut-être aussi exécuter "is_uploaded_file" sur le fichier $ _FILES ["mes_fichiers"] ["nom_mpT"]. Voir
    http://php.net/manual/en/function.is-uploaded-file.php

En voici une grande partie, mais ce n’est pas tout. (Si vous connaissez quelque chose de plus qui pourrait aider à rendre le téléchargement encore plus sûr, partagez-le.)

THIS IS QUOI WE OBTENU MAINTENANT

  • PHP principal:

    function uploadFile ($file_field = null, $check_image = false, $random_name = false) {
    
    //Config Section    
    //Set file upload path
    $path = 'uploads/'; //with trailing slash
    //Set max file size in bytes
    $max_size = 1000000;
    //Set default file extension whitelist
    $whitelist_ext = array('jpeg','jpg','png','gif');
    //Set default file type whitelist
    $whitelist_type = array('image/jpeg', 'image/jpg', 'image/png','image/gif');
    
    //The Validation
    // Create an array to hold any output
    $out = array('error'=>null);
    
    if (!$file_field) {
      $out['error'][] = "Please specify a valid form field name";           
    }
    
    if (!$path) {
      $out['error'][] = "Please specify a valid upload path";               
    }
    
    if (count($out['error'])>0) {
      return $out;
    }
    
    //Make sure that there is a file
    if((!empty($_FILES[$file_field])) && ($_FILES[$file_field]['error'] == 0)) {
    
    // Get filename
    $file_info = pathinfo($_FILES[$file_field]['name']);
    $name = $file_info['filename'];
    $ext = $file_info['extension'];
    
    //Check file has the right extension           
    if (!in_array($ext, $whitelist_ext)) {
      $out['error'][] = "Invalid file Extension";
    }
    
    //Check that the file is of the right type
    if (!in_array($_FILES[$file_field]["type"], $whitelist_type)) {
      $out['error'][] = "Invalid file Type";
    }
    
    //Check that the file is not too big
    if ($_FILES[$file_field]["size"] > $max_size) {
      $out['error'][] = "File is too big";
    }
    
    //If $check image is set as true
    if ($check_image) {
      if (!getimagesize($_FILES[$file_field]['tmp_name'])) {
        $out['error'][] = "Uploaded file is not a valid image";
      }
    }
    
    //Create full filename including path
    if ($random_name) {
      // Generate random filename
      $tmp = str_replace(array('.',' '), array('',''), microtime());
    
      if (!$tmp || $tmp == '') {
        $out['error'][] = "File must have a name";
      }     
      $newname = $tmp.'.'.$ext;                                
    } else {
        $newname = $name.'.'.$ext;
    }
    
    //Check if file already exists on server
    if (file_exists($path.$newname)) {
      $out['error'][] = "A file with this name already exists";
    }
    
    if (count($out['error'])>0) {
      //The file has not correctly validated
      return $out;
    } 
    
    if (move_uploaded_file($_FILES[$file_field]['tmp_name'], $path.$newname)) {
      //Success
      $out['filepath'] = $path;
      $out['filename'] = $newname;
      return $out;
    } else {
      $out['error'][] = "Server Error!";
    }
    
     } else {
      $out['error'][] = "No file uploaded";
      return $out;
     }      
    }
    
    
    if (isset($_POST['submit'])) {
     $file = uploadFile('file', true, true);
     if (is_array($file['error'])) {
      $message = '';
      foreach ($file['error'] as $msg) {
      $message .= '<p>'.$msg.'</p>';    
     }
    } else {
     $message = "File uploaded successfully".$newname;
    }
     echo $message;
    }
    
  • Et la forme:

    <form action="<?php echo $_SERVER['PHP_SELF']; ?>" method="post" enctype="multipart/form-data" name="form1" id="form1">
    <input name="file" type="file" id="imagee" />
    <input name="submit" type="submit" value="Upload" />
    </form>
    

Donc, ce que je demande, c’est d’aider en publiant des extraits de codes qui me permettront (à tous les autres) de rendre ce script de téléchargement d’images extrêmement sécurisé. Ou en partageant/créant un script complet avec tous les extraits ajoutés.

42
Simon

Lorsque vous commencez à travailler sur un script de téléchargement d'image sécurisé, vous devez prendre en compte de nombreux éléments. Maintenant, je ne suis pas un expert en la matière, mais on m'a demandé de le développer une fois dans le passé. Je vais parcourir tout le processus que j'ai vécu ici pour que vous puissiez suivre. Pour cela, je vais commencer par un formulaire html très basique et un script php qui gère les fichiers.

Formulaire HTML:

<form name="upload" action="upload.php" method="POST" enctype="multipart/form-data">
    Select image to upload: <input type="file" name="image">
    <input type="submit" name="upload" value="upload">
</form>

Fichier PHP:

<?php
$uploaddir = 'uploads/';

$uploadfile = $uploaddir . basename($_FILES['image']['name']);

if (move_uploaded_file($_FILES['image']['tmp_name'], $uploadfile)) {
    echo "Image succesfully uploaded.";
} else {
    echo "Image uploading failed.";
}
?> 

Premier problème: types de fichiers
Les attaquants ne doivent pas nécessairement utiliser le formulaire de votre site Web pour télécharger des fichiers sur votre serveur. POST peuvent être interceptées de différentes façons. Pensez aux addons de navigateur, aux mandataires, aux scripts Perl. Malgré tous les efforts que nous déployons, nous ne pouvons empêcher un attaquant de télécharger quelque chose ( s) il n’est pas censé le faire. Donc toute notre sécurité doit être faite côté serveur.

Le premier problème concerne les types de fichiers. Dans le script ci-dessus, un attaquant pourrait télécharger tout ce qu'il veut, comme un script php par exemple, et suivre un lien direct pour l'exécuter. Donc, pour éviter cela, nous implémentons Vérification du type de contenu:

<?php
if($_FILES['image']['type'] != "image/png") {
    echo "Only PNG images are allowed!";
    exit;
}

$uploaddir = 'uploads/';

$uploadfile = $uploaddir . basename($_FILES['image']['name']);

if (move_uploaded_file($_FILES['image']['tmp_name'], $uploadfile)) {
    echo "Image succesfully uploaded.";
} else {
    echo "Image uploading failed.";
}
?>

Malheureusement, cela ne suffit pas. Comme je l'ai déjà mentionné, l'attaquant contrôle totalement la requête. Rien ne l'empêchera de modifier les en-têtes de requête et de changer simplement le type de contenu en "image/png". Ainsi, au lieu de vous fier uniquement à l'en-tête Content-type, il serait préférable de valider également le contenu du fichier téléchargé. Voici où la bibliothèque php Gd est utile. En utilisant getimagesize(), nous allons traiter l'image avec la bibliothèque Gd. Si ce n'est pas une image, cela échouera et par conséquent tout le téléchargement échouera:

<?php
$verifyimg = getimagesize($_FILES['image']['tmp_name']);

if($verifyimg['mime'] != 'image/png') {
    echo "Only PNG images are allowed!";
    exit;
}

$uploaddir = 'uploads/';

$uploadfile = $uploaddir . basename($_FILES['image']['name']);

if (move_uploaded_file($_FILES['image']['tmp_name'], $uploadfile)) {
    echo "Image succesfully uploaded.";
} else {
    echo "Image uploading failed.";
}
?>

Nous n'y sommes toujours pas encore. La plupart des types de fichiers image autorisent l'ajout de commentaires de texte. Encore une fois, rien n'empêche l'attaquant d'ajouter du code php sous forme de commentaire. La bibliothèque Gd évaluera cela comme une image parfaitement valide. L’interprète PHP) ignorerait complètement l’image et exécuterait le code php dans le commentaire. Il est vrai que cela dépend de la configuration de php, quelles extensions de fichier sont traitées par l’interprète php et lesquelles non, mais depuis. de nombreux développeurs n’ayant aucun contrôle sur cette configuration en raison de l’utilisation d’un VPS, nous ne pouvons pas supposer que l’interprète php ne traitera pas l’image. C’est pourquoi l’ajout d’une liste blanche d’extensions de fichier n’est pas assez sûr. Soit.

La solution à cela serait de stocker les images dans un emplacement où un attaquant ne pourra pas accéder directement au fichier. Cela peut être en dehors de la racine du document ou dans un répertoire protégé par un fichier .htaccess:

order deny,allow
deny from all
allow from 127.0.0.1

Edit: Après avoir parlé avec un autre PHP programmeur, je suggère fortement d’utiliser un dossier en dehors de la racine du document, car htaccess n’est pas toujours fiable.

Nous avons toujours besoin que l'utilisateur ou tout autre visiteur puisse voir l'image. Nous allons donc utiliser php pour récupérer l'image pour eux:

<?php
$uploaddir = 'uploads/';
$name = $_GET['name']; // Assuming the file name is in the URL for this example
readfile($uploaddir.$name);
?>

Deuxième problème: attaques d'inclusion de fichiers locaux
Bien que notre script soit maintenant relativement sécurisé, nous ne pouvons pas supposer que le serveur ne souffre pas d'autres vulnérabilités. Une vulnérabilité de sécurité commune est appelée inclusion de fichier local. Pour expliquer cela, j'ai besoin d'ajouter un exemple de code:

<?php
if(isset($_COOKIE['lang'])) {
   $lang = $_COOKIE['lang'];
} elseif (isset($_GET['lang'])) {
   $lang = $_GET['lang'];
} else {
   $lang = 'english';
}

include("language/$lang.php");
?>

Dans cet exemple, nous parlons d'un site Web multilingue. La langue des sites n’est pas considérée comme une information "à haut risque". Nous essayons d'obtenir la langue préférée des visiteurs via un cookie ou une demande GET et d'inclure le fichier requis en fonction de celui-ci. Considérons maintenant ce qui se passera lorsque l’attaquant aura entré l’URL suivante:

www.example.com/index.php?lang=../uploads/my_evil_image.jpg

PHP inclura le fichier téléchargé par l’attaquant en ignorant le fait qu’il ne peut pas accéder directement au fichier et nous revenons à la case départ.

La solution à ce problème consiste à s’assurer que l’utilisateur ne connaît pas le nom du fichier sur le serveur. Au lieu de cela, nous allons changer le nom du fichier et même l'extension en utilisant une base de données pour en garder une trace:

CREATE TABLE `uploads` (
    `id` INT(11) NOT NULL AUTO_INCREMENT,
    `name` VARCHAR(64) NOT NULL,
    `original_name` VARCHAR(64) NOT NULL,
    `mime_type` VARCHAR(20) NOT NULL,
    PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8;


<?php

if(!empty($_POST['upload']) && !empty($_FILES['image']) && $_FILES['image']['error'] == 0)) {

    $uploaddir = 'uploads/';

    /* Generates random filename and extension */
    function tempnam_sfx($path, $suffix){
        do {
            $file = $path."/".mt_Rand().$suffix;
            $fp = @fopen($file, 'x');
        }
        while(!$fp);

        fclose($fp);
        return $file;
    }

    /* Process image with Gd library */
    $verifyimg = getimagesize($_FILES['image']['tmp_name']);

    /* Make sure the MIME type is an image */
    $pattern = "#^(image/)[^\s\n<]+$#i";

    if(!preg_match($pattern, $verifyimg['mime']){
        die("Only image files are allowed!");
    }

    /* Rename both the image and the extension */
    $uploadfile = tempnam_sfx($uploaddir, ".tmp");

    /* Upload the file to a secure directory with the new name and extension */
    if (move_uploaded_file($_FILES['image']['tmp_name'], $uploadfile)) {

        /* Setup a database connection with PDO */
        $dbhost = "localhost";
        $dbuser = "";
        $dbpass = "";
        $dbname = "";

        // Set DSN
        $dsn = 'mysql:Host='.$dbhost.';dbname='.$dbname;

        // Set options
        $options = array(
            PDO::ATTR_PERSISTENT    => true,
            PDO::ATTR_ERRMODE       => PDO::ERRMODE_EXCEPTION
        );

        try {
            $db = new PDO($dsn, $dbuser, $dbpass, $options);
        }
        catch(PDOException $e){
            die("Error!: " . $e->getMessage());
        }

        /* Setup query */
        $query = 'INSERT INTO uploads (name, original_name, mime_type) VALUES (:name, :oriname, :mime)';

        /* Prepare query */
        $db->prepare($query);

        /* Bind parameters */
        $db->bindParam(':name', basename($uploadfile));
        $db->bindParam(':oriname', basename($_FILES['image']['name']));
        $db->bindParam(':mime', $_FILES['image']['type']);

        /* Execute query */
        try {
            $db->execute();
        }
        catch(PDOException $e){
            // Remove the uploaded file
            unlink($uploadfile);

            die("Error!: " . $e->getMessage());
        }
    } else {
        die("Image upload failed!");
    }
}
?>

Alors maintenant, nous avons fait ce qui suit:

  • Nous avons créé un endroit sécurisé pour enregistrer les images
  • Nous avons traité l'image avec la bibliothèque Gd
  • Nous avons vérifié le type d'image MIME
  • Nous avons renommé le nom du fichier et changé l'extension.
  • Nous avons enregistré le nouveau nom de fichier et le nom d'origine dans notre base de données
  • Nous avons également enregistré le type MIME dans notre base de données

Nous devons encore pouvoir afficher l'image aux visiteurs. Pour cela, nous utilisons simplement la colonne id de notre base de données:

<?php

$uploaddir = 'uploads/';
$id = 1;

/* Setup a database connection with PDO */
$dbhost = "localhost";
$dbuser = "";
$dbpass = "";
$dbname = "";

// Set DSN
$dsn = 'mysql:Host='.$dbhost.';dbname='.$dbname;

// Set options
$options = array(
    PDO::ATTR_PERSISTENT    => true,
    PDO::ATTR_ERRMODE       => PDO::ERRMODE_EXCEPTION
);

try {
    $db = new PDO($dsn, $dbuser, $dbpass, $options);
}
catch(PDOException $e){
    die("Error!: " . $e->getMessage());
}

/* Setup query */
$query = 'SELECT name, original_name, mime_type FROM uploads WHERE id=:id';

/* Prepare query */
$db->prepare($query);

/* Bind parameters */
$db->bindParam(':id', $id);

/* Execute query */
try {
    $db->execute();
    $result = $db->fetch(PDO::FETCH_ASSOC);
}
catch(PDOException $e){
    die("Error!: " . $e->getMessage());
}

/* Get the original filename */
$newfile = $result['original_name'];

/* Send headers and file to visitor */
header('Content-Description: File Transfer');
header('Content-Disposition: attachment; filename='.basename($newfile));
header('Expires: 0');
header('Cache-Control: must-revalidate');
header('Pragma: public');
header('Content-Length: ' . filesize($uploaddir.$result['name']));
header("Content-Type: " . $result['mime_type']);
readfile($uploaddir.$result['name']);
?>

Grâce à ce script, le visiteur pourra voir l’image ou la télécharger avec son nom de fichier original. Cependant, il ne peut pas accéder directement au fichier sur votre serveur et ne sera pas en mesure de duper votre serveur pour qu'il accède au fichier, car il n'a aucun moyen de savoir de quel fichier il s'agit. . Il ne peut pas forcer brutalement votre répertoire de téléchargement, car il ne permet à personne d'accéder au répertoire, à l'exception du serveur lui-même.

Et ceci conclut mon script de téléchargement d'image sécurisé.

J'aimerais ajouter que je n'ai pas inclus de taille de fichier maximale dans ce script, mais vous devriez pouvoir le faire vous-même.

Classe ImageUpload
En raison de la forte demande de ce script, j'ai écrit une classe ImageUpload qui devrait vous permettre de gérer plus facilement les images téléchargées par les visiteurs de votre site Web. La classe peut gérer à la fois des fichiers uniques et multiples, et vous offre des fonctionnalités supplémentaires telles que l'affichage, le téléchargement et la suppression d'images.

Comme le code est trop volumineux pour être posté ici, vous pouvez télécharger le cours depuis MEGA ici:

Télécharger la classe ImageUpload

Il suffit de lire le fichier README.txt et de suivre les instructions.

Go Open Source
Le projet de classe Image Secure est désormais disponible sur mon profil Github . Ceci afin que d’autres (vous?) Puissent contribuer au projet et en faire une excellente bibliothèque pour tous. (actuellement bogué. Veuillez utiliser le téléchargement ci-dessus jusqu'à ce que corrigé).

78
icecub

Pour télécharger des fichiers dans PHP est facile et sécurisé. Je vous conseillerais de vous renseigner sur:

  • pathinfo - Retourne des informations sur un chemin de fichier
  • move_uploaded_file - Déplace un fichier téléchargé vers un nouvel emplacement
  • copie - Copie un fichier
  • finfo_open - Crée une nouvelle ressource fileinfo

Pour charger un fichier dans PHP vous avez deux méthodes: PUT et POST. Pour utiliser la méthode POST avec HTML, vous devez activer enctype sur votre formulaire comme ceci:

<form action="" method="post" enctype="multipart/form-data">
  <input type="file" name="file">
  <input type="submit" value="Upload">
</form>

Ensuite, en vous PHP vous devez récupérer votre fichier téléchargé avec $_FILES comme ceci:

$_FILES['file']

Ensuite, vous devez déplacer le fichier de temp ("upload") avec move_uploaded_file:

if (move_uploaded_file($_FILES['file']['tmp_name'], YOUR_PATH)) {
   // ...
}

Et après avoir téléchargé le fichier, vous devez vérifier l’extension du fichier. La meilleure façon de faire est d'utiliser pathinfo comme ceci:

$extension = pathinfo($_FILES['file']['tmp_name'], PATHINFO_EXTENSION);

Mais l'extension n'est pas sécurisée car vous pouvez télécharger un fichier avec l'extension .jpg mais avec le type MIME text/php et ceci est une porte dérobée. Donc, je vous recommande de vérifier le type MIME réel avec finfo_open comme ça:

$mime = finfo_file(finfo_open(FILEINFO_MIME_TYPE), $_FILES['file']['tmp_name']);

Et n'utilisez pas $_FILES['file']['type'] parce que, parfois, en fonction de votre navigateur et du système d'exploitation client, vous pouvez recevoir application/octet-stream et ce type MIME n'est pas le véritable type MIME de votre fichier téléchargé.

Je pense que vous pouvez télécharger des fichiers en toute sécurité avec ce scénario.

Désolé pour mon anglais, au revoir!

5