gfrobenius
gfrobenius

Reputation: 4067

PHP Securing Temp Files for Download

I'm semi-new to PHP and I'm starting to dive into file downloading. I create .xlsx and .csv files using PHPExcel and place them in a temp directory to be downloaded. I found a nice script for doing the download and I added some tweaks to it that I needed. The script is below. I've already read these posts:

Secure file download in PHP, deny user without permission ...and... Secure files for download ...and... http://www.richnetapps.com/the-right-way-to-handle-file-downloads-in-php/

download.php

<?php

/*====================
START: Security Checks
====================*/

//(1) Make user it's an authenicated/signed in user with permissions to do this action.
require("lib_protected_page.php");

//(2) Make sure they can ONLY download .xlsx and .csv files
$ext = pathinfo($_GET['file'], PATHINFO_EXTENSION);
if($ext != 'xlsx' && $ext != 'csv') die('Permission Denied.');

//(3) Make sure they can ONLY download files from the tempFiles directory
$file = 'tempFiles/'.$_GET['file'];

//ABOUT ITEM 3 - I still need to change this per this post I found....
/*
http://www.richnetapps.com/the-right-way-to-handle-file-downloads-in-php/

You might think you’re being extra clever by doing something like
$mypath = '/mysecretpath/' .  $_GET['file'];
but an attacker can use relative paths to evade that.
What you must do – always – is sanitize the input. Accept only file names, like this:
$path_parts = pathinfo($_GET['file']);
$file_name  = $path_parts['basename'];
$file_path  = '/mysecretpath/' . $file_name;
And work only with the file name and add the path to it youserlf.
Even better would be to accept only numeric IDs and get the file path and name from a
database (or even a text file or key=>value array if it’s something that doesn’t change
often). Anything is better than blindly accept requests.
If you need to restrict access to a file, you should generate encrypted, one-time IDs, so you can be sure a generated path can be used only once.
*/

/*====================
END: Security Checks
====================*/

download_file($file);

function download_file( $fullPath )
{
    // Must be fresh start
    if( headers_sent() ) die('Headers Sent');

    // Required for some browsers
    if(ini_get('zlib.output_compression'))
        ini_set('zlib.output_compression', 'Off');

    // File Exists?
    if( file_exists($fullPath) )
    {
        // Parse Info / Get Extension
        $fsize = filesize($fullPath);
        $path_parts = pathinfo($fullPath);
        $ext = strtolower($path_parts["extension"]);

        // Determine Content Type
        switch ($ext) {
            case "pdf": $ctype="text/csv"; break;
            case "pdf": $ctype="application/pdf"; break;
            case "exe": $ctype="application/octet-stream"; break;
            case "zip": $ctype="application/zip"; break;
            case "doc": $ctype="application/msword"; break;
            case "xls": $ctype="application/vnd.ms-excel"; break;
            case "xlsx": $ctype="application/vnd.ms-excel"; break;
            case "ppt": $ctype="application/vnd.ms-powerpoint"; break;
            case "gif": $ctype="image/gif"; break;
            case "png": $ctype="image/png"; break;
            case "jpeg":
            case "jpg": $ctype="image/jpg"; break;
            default: $ctype="application/force-download";
        }

        header("Pragma: public"); // required
        header("Expires: 0");
        header("Cache-Control: must-revalidate, post-check=0, pre-check=0");
        //Now, the use of Cache-Control is wrong in this case, especially to both values set to zero, according to Microsoft, but it works in IE6 and IE7 and later ignores it so no harm done.
        header("Cache-Control: private",false); // required for certain browsers
        header("Content-Type: $ctype");
        header("Content-Disposition: attachment; filename=\"".basename($fullPath)."\";" );
        //Note: the quotes in the filename are required in case the file may contain spaces.
        header("Content-Transfer-Encoding: binary");
        header("Content-Length: ".$fsize);
        ob_clean();
        flush();
        readfile( $fullPath );
    }
else
    die('File Not Found');
}
?>

My questions are...

  1. Are my security checks enough? I only want authenticated users with proper permissions to be able to download .xlsx and .csv files from only the tempFiles directory. But I've read that download-able files should be outside the webroot, why? With these checks I don't see why that would matter?
  2. The tempFiles directory is forbidden if you type it in on the address bar (www.mySite.com/tempFiles), but if the user somehow guesses a filename (which would be difficult, they are long and unique) then they could type that in on the address bar and get the file (www.mySite.com/tempFiles/iGuessedIt012345.csv). So is there a way to not allow that (I'm running Apache), so they are forced to go through my script (download.php)?

Thank you! Security is my number 1 concern so I want to learn every little thing I can about this before going live. Some of the example download scripts I've seen literally would let you pass in a php filename thus allowing people to steal your source code. FYI, I do clean up the tempFiles directory fairly regularly. Just leaving files there forever would be a security issue.

Upvotes: 4

Views: 3659

Answers (5)

Nemesis
Nemesis

Reputation: 22

An authenticated user can access a page(download.php) where he can view files in tempFiles

Set .htaccess to "deny from all" in tempFiles so noone can directly access, then in download.php every file should be downloadable with a token, as sad by Ashish Awasthi

If you don't like tokens you can do something like download?file=iGuessedIt012345.csv, but if you do this way use a whitelist regex to check if is everything right!

example:

$var="iGuessedIt012345.csv";

if (preg_match('#^[[:alnum:]]+\.csv$#i', $var)){
    echo "ok";
}else{
    echo "bad request";
}

example2:

$var="iGuessed_It-012345.csv";

if (preg_match('#^[a-zA-Z0-9\-\_]+\.csv$#i', $var)){
    echo "ok";
}else{
    echo "bad request";
}

Upvotes: 0

szapio
szapio

Reputation: 1008

Another option is to generate file content, not saving it on server but push that content to client browser with proper headers so client browser can interpret it as file to download. In the end client got his file without accessing your tmp folder and you don't have to worry about cleaning tmp, it's secure because you are not saving anything on your server, "data that you don't have cannot be stolen".

Example for pdf:

....
$content = $MyPDFCreator->getContent();

header('Content-Type: application/pdf');
header('Content-Length: '.strlen( $content ));
header('Content-disposition: inline; filename="downloadme.pdf"');
header('Cache-Control: public, must-revalidate, max-age=0');
header('Pragma: public');
header('Expires: Sat, 26 Jul 1997 05:00:00 GMT');
header('Last-Modified: '.gmdate('D, d M Y H:i:s').' GMT');

echo $content;

Upvotes: 3

Denis St-Michel
Denis St-Michel

Reputation: 79

I suggest that you should not let the user request a file by providing with a full path.

Filter the 'file' parameters. Make sure it doesn't start with dots, to avoid people requesting relative path to other files.

In your line:

$file = 'tempFiles/'.$_GET['file'];

if the user is requesting the file "../../var/www/my-site/index.php" for exemple, the value of your $file variable will become the path to the index.php file, given that your tempfiles/ directory is located two level deeper than your/var/www.

This is just an example, you should get the idea.

So the most important thing in my humble opinion is to filter the file parameter received. You can check for the presence of two dots (..) in the file parameters this way:

if (strpos($GET['file'], "..") !== false) {
  // file parameters contains ..
}

If, as suggested by Ashish, you can develop populate a database table with a token associated to a file and a user, then you could increment the number of time that user requests the file. After a certain amount of download, you could then deny the download request.

This approach let you keep a certain control over the downloads of file, while still giving your user some flexibility, for example if the user is accessing your web application from different location/devices and need to download the same file a few time.

Upvotes: 2

Ashish Awasthi
Ashish Awasthi

Reputation: 1502

I think you should provide a unique url with a temporary token to download the file. This token should be one time use token. Once the user have used that token it should be invalidated and if user want to download the file he need to regenerate the download link with only provided and authenticated way.

For example you can give a url like:

http://www.somedomain.com/download.php?one_time_token=<some one time token>

Once the url is visited, you should invalidate the given token. I think using this token method you can secure your file download process. For the file location You should avoid storing files on public accessible places. You should store files at some other place in you system and read you file from there only.

Upvotes: 0

ʰᵈˑ
ʰᵈˑ

Reputation: 11375

Having your files in the webroot will allow visitors to directly access your files (as they can 'run' anything in the webroot - within reason). What would be best would to have this kind of set-up, or one like it;

/var
  /www
    /html
      /my-site
        index.php
        download.php
        ...
  /tmpFiles
   iGuessedIt012345.csv

This way - with some configuration - the outside world can't get to tmpFiles/. This would also allow you to do your checks for authenticated users with correct permissions in download.php

Another way in which you could keep them out of the tmpFiles/ directory would to have a .htaccess file in that directory, with the following;

deny from all

This will yield a 403 Forbidden message to anything who'd tried to access that directory.

Upvotes: 1

Related Questions