I'm working to develop a website that allows clients to log in and see various PDFs saved on the server. These PDFs will be unique to the client and should not be accessible by someone who is not logged in. Getting the files onto the server shouldn't be an issue, I'm just not sure on how to serve them to end users.
I've implemented this kind of thing with data from SQL servers being served instead of files, so I'm not entirely sure what the most effective way to go about this.
The website is on a LAMP and my minimal experience is in PHP (but if a framework or other language would make this easier, I can learn it).
I'm probably in over my head but I usually am, so any input would be great.
Put the files outside of the webroot. Then using PHP pass the file though a script. That way no one can link to the file directly and bypass your controls. (Naturally make sure the script that does this only after verifying the user has permission to retrieve that file).
Sample PHP:
<?php
session_start();
if (!isset($_SESSION['authenticated'])) {
exit;
}
$file = '/path/to/file/outside/www/secret.pdf';
header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename=' . basename($file));
header('Content-Transfer-Encoding: binary');
header('Expires: 0');
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
header('Pragma: public');
header('Content-Length: ' . filesize($file));
ob_clean();
flush();
readfile($file);
exit;
?>
The easy way is to give these files long random filenames (say, 20 random characters). Technically they will be accessible by anyone, but it will not be possible to guess the URL, so only authorized people will have access.
Alternatively John Conde already outlined a way to serve a file from a PHP script. It will incur a small performance penalty, but will be as secure as your code is. The only thing I can add is that if you cannot place them outside webroot, then you might be able to use .htaccess to prevent people from accessing the files directly.
John posted the primary correct way of doing it, so I'm adding the (probably inferior) alternative: Serve it from the database. Just have a BLOB column for the PDF, and read/store the file data from the database. You'll end up with a quite large table, but it will work. Serving it requires setting the same header()s as John posted, you just send off the data from the DB instead of from the file.
This has the advantage of not having to ensure you don't have filename collisions, etc.
Related
I would like to trigger an action when Apache detects that a certain file URL has been started for download (or: successfully downloaded).
Example: when https://example.com/download/token_A6FZ523/myfile.zip is downloaded by a client, execute the following query to a SQLite database:
INSERT INTO downloads(date, tokenID) VALUES(CURRENT_TIMESTAMP, "A6FZ523");
Usage: then, in a PHP Dashboard, I can check who has downloaded the delivered files.
I could do this by:
running a script every minute on the server,
parsing the Apache logs /var/log/apache2/other_vhosts_access.log in search for a pattern download/token_.*/myfile.zip
execute the INSERT INTO query in this case
This seems rather complex and the fact of having to run the script every minute is not a nice solution.
What is a good solution to ask Apache to save to a SQLite database the information "The file associated to download token A6FZ523 has been downloaded by the client."?
Or maybe should PHP be used instead?
I think your problem lies in that you are directly fetching a file that is stored on the server, as opposed to using PHP to "serve" this file programatically. This isn't the first problem you will encounter with this method, you also can't check for security or get the file from external file storage (generally speaking, you don't store files directly on the web server these days!).
But, simple to do once you know how :)
Firstly, lets change the URL you download your file from to something like https://example.com/download.php?token=A6FZ523
So, we are sending a GET variable to a php script named "download.php". In that script you will have something like the following:
<?php
$token = $_GET['token'];
// Get the information about the file from the DB, something like:
// SELECT filename, size, path FROM files WHERE token = $token;
// Giving you $filename, $size and $path
header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename=' . $filename);
header('Content-Transfer-Encoding: binary');
header('Connection: Keep-Alive');
header('Expires: 0');
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
header('Pragma: public');
header('Content-Length: ' . $size);
echo file_get_contents($path);
// This will be on a completed download
// INSERT INTO downloads(date, tokenID) VALUES(CURRENT_TIMESTAMP, $token);
?>
When the download.php file is called, the token is taken and matched to a file's info in the DB. You then set headers which basically tells the browser "this is a file", your browser responds accordingly by implementing a file download as normal. You then read the contents of the file to the user. Once this has been completed, you can log the download via another DB call.
A big thing to say is that this script (obviously with the DB calls written in) should do the very basics for you, but there is a lot more to add depending on your usage scenario. Think security, input validation, where you store your files and sending a MIME type header.
Hopefully that should point you in the right direction though :)
If you have access to server and authority to install thins you could add mod_log_sql and have the apache save the log directly into a database table (it even parse the info for you) them in your dashboard you can just do simple queries. The "thing" here it seams that you are in need to get the name of the downloader, therefore you should add that "tokenID" to your URL and set the Apache to deny the url if tokenID is not present. You would need to parse the tokenID from url in the log thought.
I have a program that users can download from my site via a button which posts to a php page. The php logs the download request into my DB then serves up the actual program for them to download. However, I've noticed from time to time that certain IPs will download the program every half hour...sometimes hundreds of times over many days. Not sure what it is, assuming it's a bot, and the IPs are always in countries like Romania or Hungary.
Initially I was blocking IPs in my .htaccess, but I don't want to keep doing that every time. So I've added code to my php which only allows users to download the program a specific # of times each day. That works fine, however, it's easy enough for someone to just get the direct url to my program and download it that way bypassing the php logic.
1) Is there are way to prevent this? Can the .htaccess be modified to prevent direct downloads of the file but allow my php to serve it up?
2) Should I even be worried about this at all? I'm using a shared server so I'm really just concerned about the bandwidth impacts.
If what you want is not allowing users to bypass the PHP logic, you can render and output the file with PHP script.
<?php
$file = some file from query;
if (some logic matches)
die('Download forbidden');
if (file_exists($file)) {
header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="'.basename($file).'"');
header('Expires: 0');
header('Cache-Control: must-revalidate');
header('Pragma: public');
header('Content-Length: ' . filesize($file));
readfile($file);
exit;
}
?>
See: http://php.net/manual/en/function.readfile.php
Suppose your file is stored in /files/, put the script in somewhere like /down.php, and add the following code in .htaccess in /files/.
Deny from all
More on this, you can see: Deny access to one specific folder in .htaccess
Besides, if you really care about the bandwidth, you can enhance your download prohibit logic, like create user system, or put the user IP into the database to manage/restrict the total download bandwidth of each user.
I don't actually now hoy to ask this question, so it may be probably repeated. Let's see: I would like to disable downloading a file from my web without a download script (just using the URL: http://something/file.zip) unless you're registered, with PHP preferably. Yes, it's a very common topic but I haven't found any information! A lot of pages do this, such as uploaded.net. I hope you understand what I'm talking about. Thanks!
First and foremost, don't allow direct access to the file. Store it outside of your web application's root folder, elsewhere on the file system, so that there is no link which can be used to download it. This is because direct access skips any PHP application and interacts only with the web server, which has no knowledge of your application's session values.
Then create a "download" script to serve the file to users. Generally such a script would be given some identifier for the file, something like:
http://yourserver.com/download.php?file=file.zip
(Important: Be very careful how you identify that file. Do not just blindly let users download whatever they want, or they can enter longer paths onto the URL and download any file from your server. Always validate access to things first.)
This would be just like any other PHP script, except that instead of displaying HTML it would return a file. The actual part of outputting the file can be as simple as:
readfile('/path/to/file.zip');
You'd also likely want to set content headers appropriately, etc. A more complete example can be found in the documentation:
<?php
$file = 'monkey.gif';
if (file_exists($file)) {
header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="'.basename($file).'"');
header('Expires: 0');
header('Cache-Control: must-revalidate');
header('Pragma: public');
header('Content-Length: ' . filesize($file));
readfile($file);
exit;
}
?>
I made a form that allows user to upload a file (text documents). I’m using an unique (?) file name made from a combination of time() and the user id (only logged user can upload).
My problem is that the file cannot be accessed externally. That is, only the user who uploaded it or an admin can see it, while it can’t be reached while simply typing www.domain.com/uploads/file_name.txt
I know I can prevent the access to file through htaccess, but if I did understand it correctly, in that way I couldn’t open it even after I am logged in as admin (or as the user who sent the file).
I know I could open the file locally through php so I could show up the content through my admin panel, but that’s a pain since I could output only plain text files without problems. Also I could not download the file.
I could generate on the fly pdf or rtf versions in some cases, however that would quite a long way since I would need to elaborate the content in a complex way. And anyway, I would have no idea how to handle Word or OpenOffice files, which are likely to be the most common cases, and how to not loose formatting or other possible features.
Any ideas?
Why not display a download link for logged in users, like www.domain.com/download.php?file=... The code could look something like:
if( isset($_GET['file']) && user_is_logged_in() ) {
$file = DIR_SOME_WHERE .'/'. basename($_GET['file']);
if( file_exists($file) && user_has_file_access( $file ) ) {
header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename='.basename($file));
header('Expires: 0');
header('Cache-Control: must-revalidate');
header('Content-Length: ' . filesize($file));
ob_clean();
flush();
readfile($file);
exit;
}
}
I have files stored outside the public_html folder for security purposes. However, I would like to link to specific files somehow where a user could download one of these files.
I am using a jquery script that allows me to specify a server PATH as an upload folder, and it does upload outside the public_html folder.
The only problem is it requires me to specify a URL to the "upload path" which is used to download the files. I thought I might be able to something like:
public_html/redirect (contains htaccess which forwards all requests to "hiding" folder)
hiding (outside public_html)
A user clicks /redirect/file.doc and they download a file located at hiding/file.doc
Is this possible? If not, how can I give specific file download access to files outside of my public_html directory? I know I've seen it done on other scripts before...
You can do this with "php download handler":
You can use method like this one to return file contents and file information headers to users browser, just make sure that nothing else is outputted before this.
I suggest that you put this to separate file and call that for example download.php.
function returnFile( $filename ) {
// Check if file exists, if it is not here return false:
if ( !file_exists( $filename )) return false;
header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream');
// Suggest better filename for browser to use when saving file:
header('Content-Disposition: attachment; filename='.basename($filename));
header('Content-Transfer-Encoding: binary');
// Caching headers:
header('Expires: 0');
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
header('Pragma: public');
// This should be set:
header('Content-Length: ' . filesize($filename));
// Clean output buffer without sending it, alternatively you can do ob_end_clean(); to also turn off buffering.
ob_clean();
// And flush buffers, don't know actually why but php manual seems recommending it:
flush();
// Read file and output it's contents:
readfile( $filename );
// You need to exit after that or at least make sure that anything other is not echoed out:
exit;
}
Extending it for basic use:
// Added to download.php
if (isset($_GET['file'])) {
$filename = '/home/username/public_files/'.$_GET['file'];
returnFile( $filename );
}
Warning:
This is basic example and does not take into account that user may try to take some evil advantages of $_GET that is not properly sanitized.
This means basically that user can for example retrieve passwd file or some other sensitive information if certain conditions apply.
For example, retrieving /etc/passwd:
Just point browser to http://server.com/download.php?file=../../../etc/passwd and server returns that file. So before real use you should find out how to properly check and sanitize any user supplied arguments.
It is not possible to for paths outside the public_html.
mod_rewrite only rewrites the request, but the path still should be available to the users.
Another standard way to do this is using mod_xsendfile -- it will allow a web application to have the web server send a file as its output by specifying the path in a header (X-SendFile).