Reputation: 4067
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...
.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?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
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
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
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
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