I have been assigned a new task by a client, a document sharing application to be made using the MVC design pattern. Here are the requirements:
Uploads and downloads files with a browser
Store the document in db if that are more secure documents else store on the directory with options of password enabled or accessible without password
Every user will be have own document catalog / workspace from where he can be able to share documents with other users as well. and public shared area to share and upload files
Super admin will be able monitor the file upload logging for monitoring purpose.
I have rough idea but I would really like to know your thoughts about above points especially what is in bold up there.
The third point above is most important and I am not sure where to start from and how to go about logging the uploads.
I am basically asking for implementation details about the third and fourth points.
Here is how I implement this with CakePHP and it works nicely. First, I make sure my app code sits above the public html directory so that it is not exposed to the web. So basically, the only files the users have direct access to is the index.php file, the css/js and image files.
Then, I update my file management model to save and delete files on the fly:
function beforeSave() {
extract($this->data['Upload']['file']);
if(isset($name) and !empty($name)) {
$filename = time().'-'.$name;
if ($size && !$error) {
move_uploaded_file($tmp_name, APP . 'media/files/' . $filename);
$this->data['Upload']['file'] = $filename;
$this->data['Upload']['name'] = $name;
$this->data['Upload']['file_type'] = $type;
}
} else {
// remove the photo so it is not updated
unset($this->data['Upload']['file']);
}
return parent::beforeSave();
}
function beforeDelete() {
$data = $this->read(null, $this->id);
if( is_file( APP . 'media/files/' . $data['Upload']['file'])) {
unlink(APP . 'media/files/' . $data['Upload']['file']);
}
return true;
}
This will manage the file upload and put all of the naming information into the database. And since the app/media/files directory is not accessible to the web, I do not have to protect the directory. It means that no matter what file the user wants, they have to access it from the website.
Then, all you have to do is make sure the model table has a "shareable" flag for the user to indicate that the file is accessible to the world, and then anyone can see the file and download it.
Related
I'm a sysadmin for a small firm and I manage the server for them.
I've setup a portal for our customers to view their bills in pdf format, they are initially set with 0600 file permissions. For security reasons I cannot have all the pdf's 'visible' to everyone so I need a way to show them to the customer only when a pdf is clicked on the customers' account.
I have tried using the following, but it doesn't work and I'm getting a 'Forbidden' error...
chmod($filename, 0755);
echo "<td><iframe src='" . $filename . "' width=645 height=600 frameborder=0></iframe></td>";
chmod($filename, 0600);
The php script and the pdf files have the same owner set.
Any ideas what I'm doing wrong, I need to get this working?!
Many thanks! :)
This can not possibly work:
chmod($filename, 0755);
echo "<td><iframe src='" . $filename . "' width=645 height=600 frameborder=0></iframe></td>";
chmod($filename, 0600);
You're making the file readable only for the amount of time it takes PHP to echo that one line of HTML. I.e., by the time the user clicks the link, permissions have already been revoked again. On top of that, the file is world-readable for that period of time, so anybody on the Internet can see it.
To do this more securely, do not have the web server serve the files directly, as you will not be able to control who has access to them. Instead, put them outside the document root so that they can not be seen at all by the web server, and then proxy them through a PHP script (via readfile() or similar) that performs an ownership check.
In your script that generates the link:
echo 'PDF Download';
Where $fileId is some unique identifier for the file, but not the full file name.
Then, in download.php, something like this:
function getLoggedInUser() {
// return the logged-in user
}
function getFileForId($fileId) {
// get the full path to the file referenced by $fileId
}
function getOwnerOfFile($fileId) {
// get the user allowed to see the file referenced by $fileId
}
$fileId = $_GET['fileId'];
$file = getFileForId($fileId);
if (!file_exists($file)) {
header('HTTP/1.1 404 Not Found');
exit;
}
if (getLoggedInUser() !== getOwnerOfFile($fileId)) {
header('HTTP/1.1 403 Forbidden');
exit;
}
header('Content-type: application/pdf');
header('Content-Disposition: attachment; filename="whatever.pdf"');
readfile($file);
[UPDATE]
and I have <a href="/viewbill.php?bid=<?php echo $invoice_number; ?>" title="View PDF Invoice"> where the $invoice_number is the name of the file.
That's fine, just make sure that viewbill.php performs a check to ensure that the logged-in user is the same as the user that the bill is for, otherwise any customer can view any other customer's bills simply by changing the invoice number in the URL.
When you say 'put them outside the document root' where do you mean exactly;
Let's say that your Apache document_root directive points to /var/htdocs/public/. In this case, everything in that directory and everything under it can be seen by Apache and potentially served directly to a client. E.g., if you have a PDF file in /var/htdocs/public/pdfs/12345.pdf then a user can simply request the URL /pdfs/12345.pdf in their browser, regardless of what PHP structures are in place. Often this is mitigated with the use of .htaccess files but this is not ideal. So, if you have files that you want to keep controlled, you should not put them anywhere under the document_root. For example, put them in /var/htdocs/docs/ instead. This way, Apache can not possibly see them, but you can still use readfile() to pull their contents.
I am building a flat file cms that uses php files. Users will be able to rename files using an input field and the old and new file paths will be sent via ajax to the server where I test for security. I realize this could be done easier with regex or even OR operators. I took the OR operators out so that the strings would not be too long for this post. And as for regex, I'd like more control over the errors I send back to the client.
The CMS itself is much like a PAAS that resides in directory above all of the individual site folders that each user will have. My goal is to keep users from injecting code that might interfere with other (adjacent) user folders or the cms itself in the parent directory above.
I have not parsed the path's yet for validity. I am just trying to get an idea of how a malicious user might be able to take advantage of what I have written so far.
<?php
$old_path = $_POST['file'].'.php'; // path/to/file.php
$new_path = $_POST['new_file'].'.php'; // path/to/newfile.php';
if(strstr($new_path,"<?")){
echo "Sorry, path cannot contain script tags";
}elseif(strstr($new_path,"?>")){
echo "Sorry, path cannot contain script tags";
}elseif (strstr($new_path,">")){
echo "Sorry, path cannot contain script tags";
}elseif (strstr($new_path,"<")){
echo "Sorry, path cannot contain script tags";
}elseif($new_path[0]==="." OR $new_path[0]==='/' OR $new_path[0]==='\\'){
echo 'Sorry first character of path cannot be a period or slash';
}else{
//this is set when the user logs in based on details in a database
$users_dedicated_directory = $_SESSION['userfolder'].'/';
// add the users folder when renaming just for more control
$old_path = $users_dedicated_directory.$old_path;
// add the users folder when renaming just for more control
$new_path = $users_dedicated_directory.$old_path;
rename($old_path,$new_path);
//trim the users folder name. Send it back to the client
echo explode($users_dedicated_directory,$new_path);
}
?>
if new_path is something like a/../../path/to/one/of/you/cms/core/file.php if think a malicious user could overwrite some files of your CMS.
You'll have to remove write permissions for the web server to nay of your CMS files to prevent that.
Basically the following for the core part:
$file = basename($_GET['f']);
$directory = "/var/www/site/";
$file = $directory . $file;
$hash = $_GET['h'];
$md5check = md5($file);
$md5check = substr($md5check, 0, 5);
if ($md5check == $hash) {
if (file_exists($file)) {
unlink($file);
}
else {
die('error');
}
}
else {
header('Location: error');
exit;
}
I realise using the users input is asking for trouble, but how can I get the server to 'locate' the file to delete? Am I somehow able to escape injections?
The user would be loading http://site.com/?f=test.jpg&h=hashc
Also is there any other hash systems besides MD5 which is separate for each location of a file?
eg.
file1.rar downloaded at 12:00am = differenthash
file1.rar downloaded at 11:00pm = randomhash
file1.rar is the same file in both scenarios.
versus md5:
file1.rar downloaded at 12:00am = randomhash
file1.rar downloaded at 11:00pm = randomhash
file1.rar is the same file in both scenarios.
You're already using basename which should limit the attack vector greatly (as the user can't delete files from a different directory), however letting the user have access to delete files from /var/ is a very bad idea as the user would be able to pass any non-image file across too.
Can you not have some path relative to your web root rather than a very important system directory?
Extra security could include (note that this list is not at all exhaustive..):
User checking: Check that the web server user owns the file the user is requesting to delete.
Store uploaded files in the database and check that they exist and have been uploaded by our script before allowing deletion.
As above, move the files out of the system directory.
Use stronger hashing (ie. salts!).
Restrict this to a logged in user and log all actions, if somebody tries to delete a file it's logged and you know exactly who it was.
As #rudi_visser said, best way is white-listing, And store the uploaded files+data in the database.
And when the user tries to delete the file, make sure it's him, and make sure the file is uploaded (exists in the "uploaded" table not a part of your script/system).
I am currently building a backend to a site using the codeigniter framework, I have hit a bit of a problem, I needing a way to allow the user to upload a zipped folder of images, on completing the form, zipped folder must be unzipped, and the files need to be moved to a folder else where on the server, have thumbnail version of each image created and have there file name add to the DB, and also if the images are for a content type that does not already exist then I need to make a directory with that content type.
I know there is an upload class in Codeigniter, but I am not sure that, that has the capabilities do what I need, I could really do with some advice please?
Thanks
As Jan pointed out, this is a broad question (like 3 or 4 questions). I'm not up to date with the CodeIgniter framework but to Unzip the files you can do something like this:
function Unzip($source, $destination)
{
if (extension_loaded('zip') === true)
{
if (file_exists($source) === true)
{
$zip = new ZipArchive();
if ($zip->open($source) === true)
{
$zip->extractTo($destination);
}
return $zip->close();
}
}
return false;
}
Unzip('/path/to/uploaded.zip', '/path/to/extract/');
You wont be able to do any of the image or file checking using the Upload class. The upload class will let you accept the file and check it is a ZIP but that's as far as it will go.
From there, unzip the file and just do some simple PHP on the files to check they are the right type and make your folders etc. I would put this logic in a new library to keep it separated correctly.
I'm developing a shopping system where shopmanager should be able to upload files to the system. Those files can the be sold for a fee and should only be accesible through providing a purchase code.
The whole purchase code and uploading thing is working fine. Just have to block the direct access to the file.
Questions:
How can I allow users to upload outside of webroot but not read/download from there?
Or How do I allow users to upload to a directory but no one can read/download from it?
I'm running Apache and use code like this to upload files via a form:
public function upload_file($file='',$post_value='',$path) {
if ($_FILES[$post_value]) {
$uploadext = strtolower(strrchr($_FILES[$post_value]['name'],"."));
if($uploadext=='.jpg' || $uploadext=='.gif' || $uploadext=='.png' || $uploadext=='.swf' || $uploadext=='.jpeg' || $uploadext=='.pdf' || $uploadext=='.doc' || $uploadext=='.xls' || $uploadext=='.docx') {
$destination = $path.$file.$uploadext;
move_uploaded_file($_FILES[$post_value]['tmp_name'], $destination);
} else {
echo PICTURE_ERROR;
}
}
return $file.$uploadext;
}
you can upload where ever you want using the move_uploaded_file function, just make sure the webserver can write in the destination directory.
After you have to create a script that would read the file and pass it to the browser so you can make sure user have paid the file.
exemple
<?php
// insert your logic here to verify the user has access to this file.
// open the file in a binary mode
$name = 'yourfile';
$fp = fopen($name, 'rb');
// send the right headers
header("Content-Type: image/png");
header("Content-Length: " . filesize($name));
// dump the picture and stop the script
fpassthru($fp);
exit;
?>
You have to be careful about the content-type also make sure the user cannot every file of your server if you use a $_GET variable for getting the filename.
That's actually pretty easy. Just create a directory for your files and give apache permissions to write to it. Then when you call move_uploaded_file() function you can just specify the destination to that directory. PHP operates server side so it will be able to access that directory, while users using the browser will be limited to only what Apache will allow them to access.
If you ever need to download these files, just create a script that will parse URL parameter (or something) and take the file from the files directory and push it to the browser.
use a .htaccess file or configure in apache.conf to not allow direct access to the upload dir.
<Directory /path/to/upload/dir>
Order Deny,Allow
Deny from All
</Directory>
It would probably be easiest, in terms of securing the file, to save the files outside your webroot. When somebody wants to download it, you can use http_send_file to send the file back out to them.