PHP - making documents secure
The big problem about having your upload directory in a public place is that it's possible for visitors to directly access the files. Now imagine someone uploading a PHP script, and then visiting the file they uploaded: It gets executed as a normal PHP script, and this means full access to your server, and the real possibility to cause some serious damage.
The problem is that the files shouldn't be directly accessible. What possible solutions are there?
The easiest way would be to move the upload directory above the web root, which means it won't be accessible by visitors, but let's have a look at creating a PHP-based solution.
The first solution that comes to mind is adding our own extension to each file, making it impossible to run any PHP scripts. For example, myscript.php is stored as myscript.php.bak, which means it won't get executed by the web server as a PHP script.
But this still gives visitors the possibility to view and download files by having a look in the upload directory. We're looking for a solution that forces them to use a streaming feature.
The best solution is to encode the file, making it impossible to be viewed directly, but still possible to be downloaded through a file manager. The easiest way to do this is to change the file into a PHP script, and attaching the following line at the top of every file:
<?php header("HTTP/1.0 404 Not Found"); die(); ?>
This line makes sure that the file can't be viewed directly, but can still be opened by a file manager. The actual file data will also be encoded using the base64_encode() function so nothing harmful can be passed.
Let's start implementing this feature in ar file manager. Instead of simply copying the uploaded file to the upload directory, we must open the file, copy the data, and then write a new protected file in the upload directory, like so:
// Get file data
$data = implode('', file($_FILES['file']['tmp_name']));
// Create new 'protected' file
$file = '<?php header("HTTP/1.0 404 Not Found"); die(); ?>' . "n";
$file .= base64_encode($data);
// Write new file
$newfile = $path . $_FILES['file']['name'] . '.php';
$f = fopen($newfile, 'w');
fwrite($f, $file);
fclose($f);
// Remove temporary file
unlink($_FILES['file']['tmp_name']);
The above code first gets the file data, and then creates the new 'protected file', by adding the protection line, and the data (sent through the base64_encode() function so nothing harmful can be inserted). After that it writes the new file to the upload directory, and removes the temporary file.
Downloading files
We must also customize the streaming script somewhat, to account for the protection line and the base64 encoding. The biggest change is that the script now uses the following code:
// Get file data
$data = file($filepath);
array_shift($data);
$data = implode('', $data);
$data = base64_decode($data);
to get the file data, instead of simply using the readfile() function. The above code first gets the file data using the file() function, which means every line of the file is a separate element in an array. It then removes the protection line, as this was not part of the original file data. After it runs the data through the base64_decode() function to get back the original data.
There are a few more subtle changes, such as using the strlen() function to send the Content-Size header, instead of the filesize() function, but these are hardly worth discussing. Have a look at the full source to view all the changes.
Editing & Deleting files
The edit script also has to change slightly, because the protection line has to be removed from the file, just like with the streaming script, and the exact same code is used, i.e.:
// Get file data
$data = file($filepath);
array_shift($data);
$data = implode('', $data);
$data = base64_decode($data);
Another change in the edit script is in the way it saves the file. Just like with the upload script, it now writes a protected file, with the protection line, using the following code:
// Create new 'protected' data
$file = '<?php header("HTTP/1.0 404 Not Found"); die(); ?>' . "n";
$file .= base64_encode($_POST['newfile']);
// Write new file
$f = fopen($filepath, 'w');
fwrite($f, $file);
fclose($f);
Have a look at the source code for the complete edit script.
The delete script stays almost exactly the same, except for one minor change (each file now has a .php extension, which must be accounted for). See the source code for the change.
The index file which shows a list of all the uploaded files, using the dir class, needs a small fix as well, because all the files have a .php extension. All we need to do is strip away the .php extension, which takes very little code:
$arr['name'] = substr($file, 0, strrpos($file, '.'));
Another thing I added to the new secure file manager is the following code, which disables the 'Magic Quotes' problem, whereby slashes were being added to any files that were being edited.
if (get_magic_quotes_gpc()) {
function stripslashes_deep($value)
{
$value = is_array($value) ?
array_map('stripslashes_deep', $value) :
stripslashes($value);
return $value;
}
$_POST = array_map('stripslashes_deep', $_POST);
$_GET = array_map('stripslashes_deep', $_GET);
$_COOKIE = array_map('stripslashes_deep', $_COOKIE);
}
The above code first checks if magic quotes is enabled, and if it is, it removes all slashes from the POST, GET and COOKIE variables.
Another way to add an extra layer of security is to place a .htaccess file in your upload directory, with the following contents:
AuthName "Private Upload Directory"
AuthType Basic
AuthUserFile /non/existing/path/.htpasswd
Require valid-user
This will password protect the upload directory, except there is no password file, so no-one will be able to access it. It's just an extra layer of protection on top of our other measures.
Conclusion
1. Put your upload directory in a non-public place, like above your webroot
2. Use a .htaccess file to password protect the upload directory, making it impossible to be read by anyone.
3. Use the PHP security measure we created in this tutorial. One disadvantage is that it will make your files up to 33% bigger (due to the base64 encoding).
If you use any of the above measures, or even several together, your file manager is guaranteed to be secure!
Source Code
<?php
//?WHAT IS THE NAME OF THIS PAGE?
$sid = "secure.php";
//WHAT IS THE PATH OF YOUR SECURE DIRECTORY?
$path="/home/mypath/my_folder_name/";
//USE THIS VARIABLE ONLY IF YOU WANT TO USE THE CODE FOR (DOWNLOADING) INDIVIDUAL FILES
$display=1;
//*************UPLOAD STUFF *****************************
function doslashes()
{
if (get_magic_quotes_gpc()) {
function stripslashes_deep($value)
{
$value = is_array($value) ?
array_map('stripslashes_deep', $value) :
stripslashes($value);
return $value;
}
$_POST = array_map('stripslashes_deep', $_POST);
$_GET = array_map('stripslashes_deep', $_GET);
$_COOKIE = array_map('stripslashes_deep', $_COOKIE);
}
}
if ($_GET['upload'])
{
doslashes();
// Is the file there
if (isset($_FILES['file']) == false OR $_FILES['file']['error'] == UPLOAD_ERR_NO_FILE) {
die('No file uploaded. Please go back and try again.');
}
// No problems?
if ($_FILES['file']['error'] != UPLOAD_ERR_OK) {
die('An error occured during upload. Please go back and try again.');
}
// Get file data
$data = implode('', file($_FILES['file']['tmp_name']));
// Create new 'protected' file
$file = '<?php header("HTTP/1.0 404 Not Found"); die(); ?>' . "n";
$file .= base64_encode($data);
// Write new file
$newfile = $path . $_FILES['file']['name'] . '.php';
$f = fopen($newfile, 'w');
fwrite($f, $file);
fclose($f);
// Remove temporary file
unlink($_FILES['file']['tmp_name']);
// And we're done, redirect to index
header ('Location:'.$sid.'?display');
}
//*****************DOWNLOAD STUFF *******************
if ($_GET['download'])
{
doslashes();
if (!isset($_GET['download'])) { die('Invalid File'); }
$file = $_GET['download'];
// Create file path
$filepath = $path . $file . '.php';
// Now check if there isn't any funny business going on
if ($filepath != realpath($filepath)) {
die('Security error! Please go back and try again.');
}
// Get file extension
$ext = explode('.', $file);
$extension = $ext[count($ext)-1];
// Try and find appropriate type
switch(strtolower($extension)) {
case 'txt': $type = 'text/plain'; break;
case "pdf": $type = 'application/pdf'; break;
case "exe": $type = 'application/octet-stream'; break;
case "zip": $type = 'application/zip'; break;
case "doc": $type = 'application/msword'; break;
case "xls": $type = 'application/vnd.ms-excel'; break;
case "ppt": $type = 'application/vnd.ms-powerpoint'; break;
case "gif": $type = 'image/gif'; break;
case "png": $type = 'image/png'; break;
case "jpg": $type = 'image/jpg'; break;
case "jpeg": $type = 'image/jpg'; break;
case "html": $type = 'text/html'; break;
default: $type = 'application/force-download';
}
// Get file data
$contents = file($filepath);
array_shift($contents);
$contents = implode('', $contents);
$contents = base64_decode($contents);
// General download headers:
header("Pragma: public"); // required
header("Expires: 0");
header("Cache-Control: must-revalidate, post-check=0, pre-check=0");
header("Cache-Control: private",false); // required for certain browsers
header("Content-Transfer-Encoding: binary");
// Filetype header
header("Content-Type: " . $type);
// Filesize header
header("Content-Length: " . strlen($contents));
// Filename header
header("Content-Disposition: attachment; filename="" . $file . "";" );
// Send file data
echo $contents;
}
//delete function
if ($_GET['delete'])
{
if (!isset($_GET['delete'])) { die('Invalid File'); }
$file = $_GET['delete'];
// Create file path
$filepath = $path . $file . '.php';
// Now check if there isn't any funny business going on
if ($filepath != realpath($filepath)) {
die('Security error! Please go back and try again.');
}
// Delete the file
unlink($filepath);
// Redirect
header ('Location:'.$sid.'?display');
}
if ($display)
{
// Include header and upload path
$title = 'File Manager';
$wombat=$title;
doslashes();
// Get files in directory
$d = dir($path);
$files = array();
while (false !== ($file = $d->read())) {
if ($file == '.' OR $file == '..' OR $file == '.htaccess') continue;
$arr = array();
$arr['name'] = substr($file, 0, strrpos($file, '.'));
$arr['path'] = $d->path . $file;
$arr['size'] = filesize($arr['path']);
$files[] = $arr;
}
sort ($files);
$wombat.="<h3>Files</h3>";
if (count($files) == 0){
$wombat.="<p>There are no files yet. Upload a new one first.</p>";
}
else
{
$wombat.="<table>";
$wombat.="<tr>";
$wombat.="<th>Name</th>";
$wombat.="<th>Size</th>";
$wombat.="<td colspan="3"> </td>";
$wombat.="</tr>";
foreach($files as $file)
{
$wombat.="<tr>";
$wombat.="<td>".htmlentities($file['name'])."</td>";
$wombat.="<td>";
// Format size:
if ($file['size'] > 1000) {
$wombat.=number_format(($file['size']/1024), 2) . ' KB';
} elseif ($file['size'] > 1000*1000) {
$wombat.=number_format(($file['size']/1024)/1024, 2) . ' MB';
} else {
$wombat.=$file['size'] . ' Bytes';
}
$wombat.="</td>";
$wombat.="<td><a href="".$sid."?download=".(urlencode($file['name']))."">";
$wombat.="<b>Download</b></a></td>";
$wombat.="<td><a href="".$sid."?delete=".(urlencode($file['name']))."">";
$wombat.="Delete</a></td>";
$wombat.="</tr>";
}
$wombat.="</table>";
}
$wombat.="<br>";
$wombat.="<h3>Upload a new file</h3>";
$wombat.="<form action="".$sid."?upload=1" method="POST" enctype="multipart/form-data">";
$wombat.="<input type="file" name="file" size="30">";
$wombat.=" <input type="submit" value="Upload File">";
$wombat.="</form>";
echo ($wombat);
}
?>

