Par Boun

Avatar de Boun

PHP : download sécurisé de fichiers

Cet article est une réponse « Créer un accès sécurisé en PHP pour un contenu volumineux » de Johan. Je propose ici une autre méthode pour sécuriser le téléchargement de tout type de fichier. Cet méthode à l’avantage par rapport à la sienne de ne pas nécessiter de copie de fichier. Il n’y a donc pas d’espace disque supplémentaire utilisé.

Le principe :

  • Un dossier contenant les médias, qui n’est pas accessibles depuis le Web.
  • Un fichier PHP qui transmet le contenu du média demandé.

Sécurité

Attention, il ne faut pas passer en paramètre le chemin direct vers votre fichier, un utilisateur
mal intentionné pourrait exploiter cela pour récupérer n’importe quel fichier sur votre serveur
Web !
Il faut envoyer un identifiant de votre choix que vous faites correspondre à un chemin vers
le fichier demandé par l’intermédiaire de votre choix (simple switch, base de données, etc.)

Transfert

Le transfert du fichier ne doit pas se faire avec n’importe quelle fonction. La fonction file_get_contents par exemple n’est pas appropriée car elle place le fichier dans une chaîne de caractères et cela peut altérer les données ! Le plus simple est d’utiliser la fonction Il faut utiliser la fonction readfile qui fait tout à votre place ! Il est important d’ajouter les bonnes en-têtes au document pour que le navigateur interprète correctement les données reçues.

<?php // Attention ! ceci doit être la toute première ligne de votre fichier PHP.
// Si votre fichier est encodé en UTF-8 pensez à l'enregistrer sans BOM !
 
// Vérification des droits d'accès, à adapter à votre code !
session_start();
if (!isset($_SESSION['un_utilisateur_est_connecte']))
  die('Erreur - Il faut être connecté.');
 
/*
* Appel d'une fonction qui récupère le chemin du fichier et son type.
* Vous pouvez aussi vérifier dans cette fonction les droits d'accès pour l'utilisateur connecté.
*/
$data = recuperer_les_infos_de_mon_fichier($_GET['id']);
if (!$data)
  die('Erreur');
 
 
// Fix pour IE et ses problèmes de cache !
header("Pragma: public");
header("Expires: 0");
header("Cache-Control: must-revalidate, post-check=0, pre-check=0");
header("Cache-Control: private",false);
switch ($data['type']) {
  case "pdf": $content_type="application/pdf"; break;
  case "zip": $content_type="application/zip"; break;
  // Pour une liste plus complète des types de fichier, voir le lien ci-dessous.
  default: die("Erreur - Type de fichier non reconnu.");
}
header("Content-Type: $content_type");
header("Content-Disposition: attachment; filename="".basename($data['fichier'])."";");
// Mode binaire : pour éviter toute altération des données
header("Content-Transfer-Encoding: binary");
// Content-Length : indiquer la taille du fichier, ce qui permet au navigateur de l'utilisateur d'estimer le temps restant.
header("Content-Length: ".@filesize($data['fichier']));
set_time_limit(0);
@readfile("$data['fichier']") or die("Fichier introuvable.");

Content-Type

Pour que le navigateur de l’utilisateur reconnaisse le fichier que vous envoyez et l’ouvre avec la bonne application, il faut spécifier le type MIME dans la directive Content-Type : lien vers une liste de types.

Problèmes éventuels

  • Sur certains serveurs ayant des restrictions de sécurités, la fonction set_time_limit peut être désactivée (cf. safe_mode de PHP). Dans ce cas, le temps d’exécution du script sera limité et pour un fichier volumineux, vous ne pourrez utiliser cette méthode.
  • Dans certains cas, Internet Explorer peut poser des problèmes en mettant tout de même le fichier dans le cache, notamment si vous utilisez le protocole HTTPS.

7 commentaires

  • Johan (38 comments), le 8 avril 2009

    Bonjour Boun,
    En effet ta méthode nettement plus rapide que la mienne et bien moins gourmande en espace.
    Si j’avais opté pour une copie des fichiers c’est en fait parce que les fichiers sources étaient susceptibles d’être modifiés ou supprimés, mais les personnes étant en train de visionner ces fichiers devaient pouvoir y avoir accès pendant 24h. J’aurais du mentionné la possibilité d’utiliser la fonction readfile associé aux headers appropriés mais tu viens de le faire à ma place :).
    Merci d’avoir apporté cette solution qui conviendra surement à plus de monde que la mienne.

  • Créer accès sécurisé en PHP pour un contenu volumineux. | LE GLOB DE BARGEO, le 8 avril 2009

    [...] Attention, la méthode que je décris ici s’applique seulement dans le cas précis où l’utilisateur doit avoir accès à un contenu pendant une certaine durée, même si le contenu source est modifié ou supprimé ! Dans tout les autre cas, il est plus judicieux d’utiliser la fonction readfile() associé a l’envoie de headers appropriés (la méthode est décrite dans l’article de Boun PHP : download sécurisé de fichiers). [...]

  • devzonefr (1 comments), le 30 septembre 2009

    salut !

    j’ai 2 remarques à faire :)

    Pour le choix “content-type” plutôt qu’un die(); j’aurai mis un :

    default: $content_type = “application/force-download”;

    histoire de ne pas bloquer le téléchargement.

    de même je préfère utiliser file_exits() pour tester l’existence du fichier plutôt que de contourner le probleme avec un @ :

    if( !file_exists($data['fichier']) )
    {
    die(“Fichier introuvable.”)
    }
    else {
    readfile($data['fichier']);
    }

    au lieu de :

    @readfile(“$data['fichier']“) or die(“Fichier introuvable.”);

    ++

  • Jeff (1 comments), le 13 juin 2012

    Bonjour,
    J’ai testé et ça ne fonctionne pas avec les fichiers lourds (le téléchargement arrête à 70% du fichier total dans mon cas).

    – Jeff

  • Boun (4 comments), le 13 juin 2012

    @Jeff

    Je ne suis pas sur, mais à priori, je dirais que c’est peut être à cause d’une limitation de mémoire, soit de PHP, soit d’Apache.
    Peut-être que pour des fichiers vraiment très volumineux, la méthode proposée par Johan serait meilleure…

    Boun

  • romain (3 comments), le 18 juin 2012

    Hello,
    moi j’utilise ce petit script que j’ai modifié afin de choisir le nom, l’extension,le renommage, ect… et qui me permet non seulement de limiter le debit de téléchargement, mais qui en cadeau découpe le flux en petit paquet permettant d’éviter de saturer la mémoire de son serveur:

    $filedir = ‘/var/www/vhosts/monsite.com/httpdocs/mondossier/videos/’;
    $local_file = $filedir.$video_filename.’.’.$video_extension;
    $download_file = ‘newname_’.$video_filename.’.’.$video_extension;

    // set the download rate limit (=> 600 kb/s)
    $download_rate = 450;
    if(file_exists($local_file) && is_file($local_file))
    {
    header(‘Cache-control: private’);
    header(‘Content-Type: application/octet-stream’);
    header(‘Content-Length: ‘.filesize($local_file));
    header(‘Content-Disposition: attachment; filename=’.$download_file);

    flush();
    $file = fopen($local_file, “r”);
    while(!feof($file))
    {
    // send the current file part to the browser
    print fread($file, round($download_rate * 1024));
    // flush the content to the browser
    flush();
    // sleep one second
    sleep(1);
    }
    fclose($file);}
    else {
    die(‘Error: The file ‘.$local_file.’ does not exist!’);
    }

    Ca marche bien pour n’importe quel fichier à télécharger, de tout poids, sur IE/FF et chrome, récents du moins :)
    merci qui?

  • js-ssiw (1 comments), le 11 janvier 2013

    Bonjour,

    j’ai mis en place ce script sur un site internet il marche nickel enfin presque…

    Le téléchargement sur un smartphone pose problème…

    Sur un smartphone, d’une au lieu de renommer le fichier à télécharger grâce à la l’instruction suivante filename=’.$download_file’ il me met download.ext (ext = pdf s’il s ‘agit d’un pdf, ext = zip s’il s’agit d’un zip, …)

    Et de plus après le téléchargement le document ne peut pas être consulté il me mets que le fichier est endommagé.

    Avez-vous la même chose ? Si oui avez-vous réussi a trouver un contournement ?

    Merci pour vos réponses.

Poster un commentaire

Subscribe without commenting