Nous avons désormais un
script qui fonctionne très bien. Malheureusement, et vous ne vous en êtes peut-être pas rendu compte, il possède une
énorme faille…
Oui, imaginez qu'un utilisateur malveillant décide de se rendre sur cette page :
http://www.monsite.com/telecharger.php [...] =../index.php, il sera alors en mesure de télécharger directement le contenu de votre page d'accueil. Pire, avec cette technique, l'ensemble des fichiers de votre site sera à sa disposition et, pour peu qu'il connaisse sa structure, il pourra faire une copie complète de votre site comme s'il avait accès à votre FTP. Il n'aurait plus qu'à récupérer les identifiants de votre base de données (que vous avez forcément stockés dans l'un de vos fichiers PHP) pour finir de voler votre site en entier !
Vous ne pensiez pas qu'un petit
script comme celui-là se révélerait aussi dangereux, n'est-ce pas ? C'est pourquoi il est important de se prémunir contre cette faille. Heureusement, je vous propose une solution permettant de pallier ce problème.
La première chose à faire est d'empêcher le visiteur d'utiliser le
slash (
/). Ainsi, il ne pourra pas retourner en arrière ou naviguer dans les dossiers à sa guise.
Oui, mais attends ! Si je fais ça, je ne pourrais plus classer les fichiers téléchargeables qui se trouvent dans le dossier fichiers/ à l'intérieur de sous-dossiers. Tous les fichiers téléchargeables ne doivent-ils pas se trouver à la racine du dossier fichiers/ ?!
Pas nécessairement. C'est à vous et à vous seul de définir les règles, et vous pouvez très bien choisir de rajouter des variables GET qui détermineront dans quel sous-dossier le fichier demandé se trouve.
Un petit exemple avec ce lien :
http://www.monsite.com/telecharger.php [...] txt&dossier=1. Il suffit de faire en sorte que l'identifiant
1 corresponde à un dossier particulier pour la variable
$_GET['dossier']. Nous pourrions organiser les dossiers de la façon suivante :
- 1 = fichiers/jeux/ ;
- 2 = fichiers/musiques/ ;
- 3 = fichiers/videos/ ;
- 4 = fichiers/images/ ;
- etc.
Ce qui, en programmation, donnerait ceci :
Code : PHP 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24 | <?php
function id_dossier($id)
{
$dossier_base = 'fichiers/';
switch($id)
{
case 1:
$dossier_base .= 'jeux/';
break;
case 2:
$dossier_base .= 'musiques/';
break;
case 3:
$dossier_base .= 'videos/';
break;
case 4:
$dossier_base .= 'images/';
break;
default:
break;
}
return $dossier_base;
}
?>
|
Mais ce n'est pas tout… Même si l'on bouche cette faille, il en reste une : quoi qu'il arrive, il y aura toujours dans votre dossier des fichiers qui ne seront pas destinés au téléchargement. C'est par exemple le cas pour notre .htaccess, qui devra forcément se trouver à cet endroit, et pour tous les fichiers cachés dont les noms commencent par un point. Il faut donc vérifier également qu'il n'y a pas de point au début du nom du fichier.
Pour ce faire, nous allons utiliser la fonction
strpos() qui permet de récupérer la position d'un caractère (ou de plusieurs caractères dans notre cas) dans une chaîne.
La position d'un caractère dans une chaîne ? À quoi cela va-t-il nous servir ?
À une seule chose : vérifier qu'il y a ou non des caractères que nous voulons interdire. Si les caractères en question ne sont pas trouvés, la fonction renverra
FALSE.
Ça, c'était pour le
slash. Pour le point, en revanche, récupérer la position du caractère va nous être utile. Non, il n'est pas interdit de mettre un point dans le nom d'un fichier, surtout si vous décidez de laisser l'extension. Par contre, nous ne voulons surtout pas que ce point se trouve au début du nom du fichier ; nous vérifierons donc la position du point.
En sachant tout cela, notre fichier de fonctions sera modifié de cette façon :
Code : PHP - fonctions.php 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44 | <?php
function id_dossier($id)
{
$dossier_base = 'fichiers/';
switch($id)
{
case 1:
$dossier_base .= 'jeux/';
break;
case 2:
$dossier_base .= 'musiques/';
break;
case 3:
$dossier_base .= 'videos/';
break;
case 4:
$dossier_base .= 'images/';
break;
default:
break;
}
return $dossier_base;
}
function telecharger_fichier($fichier, $id = 0)
{
$chemin = id_dossier($id) . $fichier;
if(file_exists($chemin) && strpos($fichier, '/') === FALSE && strpos($fichier, '.') !== 0)
{
header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename='. basename($chemin));
header('Content-Transfer-Encoding: binary');
header('Expires: 0');
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
header('Pragma: public');
header('Content-Length: ' . filesize($chemin));
readfile($chemin);
exit;
}
else
require('erreur.php');
}
?>
|
Ne vous inquiétez pas, je ne me suis pas trompé en mettant trois signes « égal ». L'opérateur === signifie « égal et de même type » et, ici, la condition est nécessaire à notre vérification, car 0 == FALSE mais 0 !== FALSE !
Nous nous assurons ainsi que l'utilisateur ne pourra pas naviguer à l'extérieur du dossier dans lequel se trouvent tous les fichiers à télécharger.
Vous ne devez donc pas — j'insiste encore sur ce point — placer dans ce dossier des fichiers qui ne sont pas censés être téléchargés, comme des fichiers PHP. Une bonne fois pour toutes : le dossier fichiers/ ne doit contenir que les fichiers que vos utilisateurs sont autorisés à télécharger !
De ces changements découle une modification minime de notre fichier
telecharger.php afin de prendre en compte l'identifiant du dossier :
Code : PHP - telecharger.php | <?php
if(!empty($_GET['fichier']))
{
// N'oubliez pas d'inclure le fichier qui contient notre fonction !
include('fonctions.php');
// On appelle la fonction qu'on a créée juste avant !
telecharger_fichier($_GET['fichier'], $_GET['dossier']);
}
else
require('erreur.php');
?>
|
La suite va vous présenter une autre solution un peu plus aboutie (à noter qu'elle reste néanmoins complémentaire de la première). Cela veut dire qu'il est hautement préférable d'appliquer cette solution en plus, et vous allez tout de suite comprendre pourquoi.
Comme je l'ai dit au début de ce cours, sur de nombreux sites, les fichiers à télécharger sont gérés par une base de données. Par conséquent, chaque fichier est lié à des informations comme un nom (plus joli que le nom du fichier), une description, un compteur, etc. Ainsi, il vous suffirait de vérifier que le fichier que veut télécharger l'utilisateur figure bien dans la base de données. Dans le cas contraire, le téléchargement n'est pas lancé.
Cette solution peut ne pas suffire lorsque vous permettez le téléchargement de fichiers PHP (pour des tutoriels par exemple) et que ces derniers portent, parfois, le même nom que ceux qui se trouvent sur votre site. Dans ce cas-là, il est important de vérifier que le fichier que le visiteur souhaite télécharger se trouve dans le bon dossier. C'est précisément ce que nous venons de faire.
Ce qui donne sous forme de code :
Code : PHP 1
2
3
4
5
6
7
8
9
10
11
12
13 | <?php
/*
On imagine que la connexion se fait avec PDO et que l'objet
PDO se trouve dans la variable du nom de $pdo
*/
$nom_fichier = $pdo->quote($_GET['fichier']);
$sql = $pdo->query("SELECT COUNT(nom_fichier) FROM fichiers WHERE nom_fichier = '$nom_fichier'");
if(file_exists($chemin) && strpos($fichier, '/') === FALSE && strpos($fichier, '.') !== 0 &&
$sql->fetchColumn() > 0)
{
// Appel de la fonction pour télécharger le fichier
}
?>
|
Il ne s'agit là que d'un exemple qui ne fonctionnera qu'à deux conditions. Premièrement, que vous vous soyez connecté avec PDO en stockant l'objet PDO dans la variable qui répond au doux nom de
$pdo ; deuxièmement, que vous possédiez une table de votre base de données, nommée
fichiers, qui comporte une colonne
nom_fichier et où sont stockés tous les fichiers téléchargeables.
Pfiou, ça en fait des conditions !
Quoi qu'il en soit, je pense que vous avez compris que cet exemple vous permet uniquement de comprendre le concept. À vous de l'adapter au fonctionnement de votre site.