31

I'm hoping to make my tiny program secure so that potential malicious users cannot view sensitive files on the server.

    $path = "/home/gsmcms/public_html/central/app/webroot/{$_GET['file']}";


    if(file_exists($path)) {
        echo file_get_contents($path);
    } else {
        header('HTTP/1.1 404 Not Found');
    }

Off the top of my head I know that input such as '../../../../../../etc/passwd' would be trouble, but wondering what other malcious inputs I should expect and how to prevent them.

Dharman
  • 30,962
  • 25
  • 85
  • 135
SeanDowney
  • 17,368
  • 20
  • 81
  • 90
  • 1
    Just exclude any input containing ../ – halfdan Dec 16 '09 at 00:06
  • 3
    Agreed that would solve one problem, but I'm assuming that there are many other hazards I need to look out for. I'm looking for a good iron clad solution to all of them – SeanDowney Dec 16 '09 at 00:14
  • 3
    @halfdan - always try to avoid a black-list approach to security like this, there will always be something you miss. Such as use of backspace chars, tabs, newlines, null chars, other unicode characters, or intentionally broken unicode chars that would pass your filter, but still cause the PHP function to do something you were trying to protect it from. Test what you really want: that the resultant path is under a safe location. – Cheekysoft Dec 16 '09 at 13:23

9 Answers9

39

realpath() will let you convert any path that may contain relative information into an absolute path...you can then ensure that path is under a certain subdirectory that you want to allow downloads from.

Bert Lamb
  • 2,317
  • 2
  • 20
  • 26
  • 5
    This was my final solution: $baseDir = "/home/gsmcms/public_html/central/app/webroot/"; $path = realpath($baseDir . $_GET['file']); // if baseDir isn't at the front 0==strpos, most likely hacking attempt if(strpos($path, $baseDir)) { die('Invalid Path'); } elseif(file_exists($path)) { echo file_get_contents($path); } else { header('HTTP/1.1 404 Not Found'); echo "The requested file could not be found"; } – SeanDowney Dec 16 '09 at 00:36
  • 2
    @SeanDowney there is a bug in your solution, namely you should check that strpos doesn't return false or non-zero, which you are not doing. – Michael Dec 08 '14 at 23:36
  • 3
    realpath() is a good friend but not enough - it only returns the full path if you're referring to an existing path. Otherwise it would return false; in some cases this won't cause any trouble but considering a "save as" operation with auto-creation of the subdirectory, it can ruin the game. (User tells you where to put the file but you respond "invalid path" since realpath returns false.) It's probably not what you're doing; just thought to mention. – dkellner Oct 02 '15 at 20:13
  • @dkellner Could you break up the validatation into two sections: the path that exists and the new automatically created directory at the end? For example: given "/this/path/exists/autoCreatedDirectory/file", split the last directory and file out and validate "/this/path/exists" with realpath. Then validate the "autoCreatedDirectory" and filename (e.g. they should not be '..', etc). – kapace Nov 02 '15 at 17:56
  • 1
    @kapace - that's a possibility but you'd have to check a lot. Since you don't know which substring exists already, you'd have to split the path into segments by every "/", then incrementally add one segment and check if what you have so far exists or not. It's rarely needed but clearly a solution. – dkellner Nov 02 '15 at 18:15
14

Use basename rather than trying to anticipate all the insecure paths a user could provide.

philfreo
  • 41,941
  • 26
  • 128
  • 141
  • this may work in some situations, however I'm expecting input to include directories as well, ex: '/js/jquery/jquery.js' – SeanDowney Dec 16 '09 at 00:28
9

Solution by the OP:

$baseDir = "/home/gsmcms/public_html/central/app/webroot/"; 
$path = realpath($baseDir . $_GET['file']); 

// if baseDir isn't at the front 0==strpos, most likely hacking attempt 
if(strpos($path, $baseDir) !== 0) { 
   die('Invalid Path'); 
} elseif(file_exists($path)) { 
   echo file_get_contents($path); 
} else { 
   header('HTTP/1.1 404 Not Found'); 
   echo "The requested file could not be found"; 
} 
Gershom Maes
  • 7,358
  • 2
  • 35
  • 55
Yauhen Yakimovich
  • 13,635
  • 8
  • 60
  • 67
  • 1
    please learn to use advanced SO features! :) it simplifies *copy-paste* – Yauhen Yakimovich Oct 30 '14 at 21:49
  • FYI, I found a bug in this solution, but my edit containing the fix was rejected. – Michael Dec 09 '14 at 14:36
  • Please check the above. It should be more explicit and justified for sanity purpose. – Yauhen Yakimovich Dec 09 '14 at 15:13
  • 2
    presumably, if `strpos($path, $baseDir) === false`, then `strpos($path, $baseDir) !== 0`, since I expect that `false !== 0` in php. So it seems the second part of the test on line 5 is not required. – PypeBros Oct 03 '17 at 11:51
  • `realpath()` returns false if the file doesn't exist so `elseif(file_exists($path)` is redundant – Haskell McRavin Mar 20 '21 at 10:08
  • It's an extreme case but I think this sample makes it possible to discover the directory structure on the machine by returning a valid response when I move up a directory and guess a directory name correctly (a "directory oracle"?) i.e. if I make a request with `file` set to `../webroot/some-valid-file` then this leaks that the files are in the `webroot` directory. I can keep guessing my way up and down the tree until I find `/home`. At that point I have a way of finding the usernames on the machine – monkeysplayingpingpong Mar 03 '23 at 22:39
6

If you can, use a whitelist like an array of allowed files and check the input against that: if the file asked by the user isn't present in that list, deny the request.

Matteo Riva
  • 24,728
  • 12
  • 72
  • 104
5

There is an additional and significant security risk here. This script will inject the source of a file into the output stream without any server-side processing. This means that all your source code of any accessible files will be leaked to the internet.

Cheekysoft
  • 35,194
  • 20
  • 73
  • 86
4

Even if you are using realpath, you should still strip all ".." before using it. Otherwise an attacker can read your servers entire directory structure with brute force, e.g. "valid_folder/../../test_if_this_folder_name_exists/valid_folder" - if the application accepts this path, the attacker knows that the folder exists.

Toxiro
  • 528
  • 1
  • 3
  • 12
1

Another approach:

$path = "/app/webroot/{$_GET['file']}";
$realTarget = realpath($path);

if( strtolower($path) !== strtolower($realTarget) ) {
    // invalid path!
}
// life goes on
algorix
  • 301
  • 3
  • 7
1

I think this is the best answer for PHP7.

This will only allow people to see files they have the absolute path to.

It won't let people fish for valid filenames outside the specified path by making all failure conditions report the same.

$base_dir = $temp_path;
$path = "";
if(isset($_GET['filename'])) {
    $path = realpath($base_dir.$_GET['filename']);
    //realpath returns false if the file doesnt exist
    if(!$path ||
    //dont look outside temp path 
        substr($path, 0, strlen($base_dir)) != $base_dir){
            header('HTTP/1.1 404 Not Found'); 
            echo "The requested file could not be found";
            die;
    }
}
Haskell McRavin
  • 620
  • 5
  • 19
0

To strip all /. /.. or \. \.. and convert to all forward slash because the different environments will accept forward slash. This should provide a fairly safe filter for path input. In your code you should be comparing it to parent directories that you do not want access just in case.

 $path = realpath(implode('/', array_map(function($value) {return trim($value, '.');}, explode('/', str_replace('\\', '/', $path)))));  
Spooky
  • 2,966
  • 8
  • 27
  • 41
snoop_dog
  • 51
  • 6
  • @Koby: Why did you edit my answer? The str_replace puts all forward slashes & the realpath makes it all single slash.. do not edit my code – snoop_dog Mar 06 '17 at 02:15