PHP 5.6.0 released

Sécurité des fichiers

Sommaire

PHP est soumis aux règles de sécurité intrinsèques de la plupart des systèmes serveurs : il respecte notamment les droits des fichiers et des dossiers. Une attention particulière doit être portée aux fichiers ou dossiers qui sont accessibles à tout le monde, afin de s'assurer qu'ils ne divulguent pas d'informations critiques.

Puisque PHP a été fait pour permettre aux utilisateurs d'accéder aux fichiers, il est possible de créer un script PHP qui vous permet de lire des fichiers tels que /etc/password, de modifier les connexions ethernet, lancer des impressions de documents, etc. Cela implique notamment que vous devez vous assurer que les fichiers manipulés par les scripts sont bien ceux qu'il faut.

Considérez le script suivant, où l'utilisateur indique qu'il souhaite effacer un fichier dans son dossier racine. Nous supposons que PHP est utilisé comme interface web pour gérer les fichiers, et que l'utilisateur Apache est autorisé à effacer les fichiers dans le dossier racine des utilisateurs.

Exemple #1 Une erreur de vérification de variable conduit à un gros problème

<?php
// Efface un fichier dans un dossier racine
$username $_POST['user_submitted_name'];
$userfile $_POST['user_submitted_filename'];
$homedir  "/home/$username";

unlink("$homedir/$userfile");

echo 
"Ce fichier a été effacé !";
?>
Étant donné que le nom de l'utilisateur et le nom du fichier sont fournis, des intrus peuvent envoyer un nom d'utilisateur et un nom de fichier autres que les leurs, et effacer des documents dans les comptes des autres utilisateurs. Dans ce cas, vous souhaiterez utiliser une autre forme d'identification. Considérez ce qui pourrait se passer si les utilisateurs passent ../etc/ et passwd comme arguments! Le code serait exécuté tel que :

Exemple #2 Une attaque du système de fichiers!

<?php
// efface un fichier n'importe où sur le disque dur,
// où l'utilisateur PHP a accès. Si PHP a un accès root :
$username $_POST['user_submitted_name']; // "../etc"
$userfile $_POST['user_submitted_filename']; // "passwd"
$homedir  "/home/$username"// "/home/../etc"

unlink("$homedir/$userfile"); // "/home/../etc/passwd"

echo "Ce fichier a été effacé !";
?>
Il y a deux mesures primordiales à prendre pour éviter ces manoeuvres :
  • Limiter les permissions de l'utilisateur web PHP.
  • Vérifier toutes les variables liées aux chemins et aux fichiers qui sont fournis.
Voici un script renforcé :

Exemple #3 Une vérification renforcée

<?php
// Efface un fichier sur le disque où l'utilisateur a le droit d'aller
$username $_SERVER['REMOTE_USER']; // utilisation d'un mécanisme d'identification
$userfile basename($_POST['user_submitted_filename']);
$homedir  "/home/$username";

$filepath "$homedir/$userfile";

if (
file_exists($filepath) && unlink($filepath)) {
   
$logstring "$filepath effacé\n";
} else {
   
$logstring "Échec lors de l'effacement de $filepath\n";
}
$fp fopen("/home/logging/filedelete.log""a");
fwrite($fp$logstring);
fclose($fp);

echo 
htmlentities($logstringENT_QUOTES);
?>
Cependant, même cette technique n'est pas sans faille. Si votre système d'identification permet aux utilisateurs de créer leur propre login, et qu'un utilisateur choisi le login ../etc/, le système est de nouveau exposé. Pour cette raison, vous pouvez essayez d'écrire un script renforcé :

Exemple #4 Vérification renforcée de noms de fichiers

<?php
$username     
$_SERVER['REMOTE_USER']; // utilisation d'un mécanisme d'identification
$userfile     $_POST['user_submitted_filename'];
$homedir      "/home/$username";

$filepath     "$homedir/$userfile";

if (!
ctype_alnum($username) || !preg_match('/^(?:[a-z0-9_-]|\.(?!\.))+$/iD'$userfile)) {
   die(
"Mauvais utilisateur/nom de fichier");
}

//etc...
?>

Suivant votre système d'exploitation, vous devrez protéger un grand nombre de fichiers, notamment les entrées de périphériques, (/dev/ ou COM1), les fichiers de configuration (fichiers /etc/ et .ini), les lieux de stockage d'informations (/home/, My Documents), etc. Pour cette raison, il est généralement plus sûr d'établir une politique qui interdit TOUT sauf ce que vous autorisez.

add a note add a note

User Contributed Notes 10 notes

up
21
anonymous
8 years ago
(A) Better not to create files or folders with user-supplied names. If you do not validate enough, you can have trouble. Instead create files and folders with randomly generated names like fg3754jk3h and store the username and this file or folder name in a table named, say, user_objects. This will ensure that whatever the user may type, the command going to the shell will contain values from a specific set only and no mischief can be done.

(B) The same applies to commands executed based on an operation that the user chooses. Better not to allow any part of the user's input to go to the command that you will execute. Instead, keep a fixed set of commands and based on what the user has input, and run those only.

For example,
(A) Keep a table named, say, user_objects with values like:
username|chosen_name   |actual_name|file_or_dir
--------|--------------|-----------|-----------
jdoe    |trekphotos    |m5fg767h67 |D
jdoe    |notes.txt     |nm4b6jh756 |F
tim1997 |_imp_ folder  |45jkh64j56 |D

and always use the actual_name in the filesystem operations rather than the user supplied names.

(B)
<?php
$op
= $_POST['op'];//after a lot of validations
$dir = $_POST['dirname'];//after a lot of validations or maybe you can use technique (A)
switch($op){
    case
"cd":
       
chdir($dir);
        break;
    case
"rd":
       
rmdir($dir);
        break;
    .....
    default:
       
mail("webmaster@example.com", "Mischief", $_SERVER['REMOTE_ADDR']." is probably attempting an attack.");
}
up
3
devik at cdi dot cz
13 years ago
Well, the fact that all users run under the same UID is a big problem. Userspace  security hacks (ala safe_mode) should not be substitution for proper kernel level security checks/accounting.
Good news: Apache 2 allows you to assign UIDs for different vhosts.
devik
up
3
fmrose at ncsu dot edu
8 years ago
All of the fixes here assume that it is necessary to allow the user to enter system sensitive information to begin with. The proper way to handle this would be to provide something like a numbered list of files to perform an unlink action on and then the chooses the matching number. There is no way for the user to specify a clever attack circumventing whatever pattern matching filename exclusion syntax that you may have.

Anytime you have a security issue, the proper behaviour is to deny all then allow specific instances, not allow all and restrict. For the simple reason that you may not think of every possible restriction.
up
4
Latchezar Tzvetkoff
5 years ago
A basic filename/directory/symlink checking may be done (and I personally do) via realpath() ...

<?php

if (isset($_GET['file'])) {
   
$base = '/home/polizei/public_html/'// it seems this one is good to be realpath too.. meaning not a symlinked path..
   
if (strpos($file = realpath($base.$_GET['file']), $base) === 0 && is_file($file)) {
       
unlink($file);
    } else {
        die(
'blah!');
    }
}
?>
up
3
eLuddite at not dot a dot real dot addressnsky dot ru
13 years ago
I think the lesson is clear:

(1) Forbit path separators in usernames.
(2) map username to a physical home directory - /home/username is fine
(3) read the home directory
(4) present only results of (3) as an option for deletion.

I have discovered a marvelous method of doing the above in php but this submission box is too small to contain it.

:-)
up
1
akul at inbox dot ru
10 years ago
Common and simple way to avoid path attack is separating objectname and filesystem space when possible. For example if I have users on my site and directory per user solution is not "httpdocs/users/$login", but "httpdocs/users/".md5($login) or "httpdocs/users/".$userId
up
0
cronos586(AT)caramail(DOT)com
12 years ago
when using Apache you might consider a apache_lookup_uri on the path, to discover the real path, regardless of any directory trickery.
then, look at the prefix, and compare with a list of allowed prefixes.
for example, my source.php for my website includes:
if(isset($doc)) {
    $apacheres = apache_lookup_uri($doc);
    $really = realpath($apacheres->filename);
    if(substr($really, 0, strlen($DOCUMENT_ROOT)) == $DOCUMENT_ROOT) {
        if(is_file($really)) {
            show_source($really);
        }
    }
}
hope this helps
regards,
KAT44
up
-4
steelchords at yahoo dot com
7 years ago
It seems to me in this particular instance that a simple check to make sure that name or partial pathname doesn't already exist would prevent this attack... if a 'passwd/etc/...' existed as the password directory, you couldn't create a username to exploit the hole in the first place.  But that's only from a 'script user' perspective, it still doesn't protect your server from other sub-admin's badly written code.

Don For
up
-4
1 at 234 dot cx
9 years ago
I don't think the filename validation solution from Jones at partykel is complete.  It certainly helps, but it doesn't address the case where the user is able to create a symlink pointing from his home directory to the root.  He might then ask to unlink "foo/etc/passwd" which would be in his home directory, except that foo is a symlink pointing to /.

Personally I wouldn't feel confident that any solution to this problem would keep my system secure.  Running PHP as root (or some equivalent which can unlink files in all users' home directories) is asking for trouble.

If you have a multi-user system and you are afraid that users may install scripts like this, try security-enhanced Linux.  It won't give total protection, but it at least makes sure that an insecure user script can only affect files which the web server is meant to have access to.  Whatever script someone installs, outsiders are not going to be able to read your password file---or remove it.
up
-7
joshudson';DROP TABLE EMAILS;' gmail.com
9 years ago
I keep application configuration files in the document root. I found the most effective trick to prevent access to them is to
1. Give them no code that actually runs when included (except for variable assignments),
2. Don't use register globals so nobody can do anything weird,
3. Name them *.php so PHP runs them when asked for
4. Don't have anything before <?php
5. Don
't have a ?>
To Top