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.
Related
I have a website where users can login and are identified by sessions and $user[id]. They can upload files. Files are stores in htdocs/uploads. Every file data is stored in a table to have a link between filename, location and user_id. On some places on my website i give the possibility to download the file by: <a href="' . $row['path'] . '" target="_blank" download>Download file</a>.
Behind $row['path'] a URL like domain.com/uploads/filename.jpg is given. If the user know this URL path he can download the file also without login or identification by entering the URL into a browser. I want to avoid this. How can i ensure, that only files can be downloaded from htdocs/uploads by click on such a download link on my website.
=> I am not able to create a folder outside htdocs
=> I have tried to change the folder permissions but w/o success
=> My website is basically made in PHP, a solution in PHP is prefered.
Any idea/help available? Thank you!
You will need to go down the route of making PHP serve the file for you. This will ensure that you can validate user credentials before serving the file.
See examples on php.net here. This will explain how to serve files from PHP using header(). This answer also outlines some of the key concepts.
Once you've done this, you can deny direct access to these files using htaccess rules. This will ensure users can only access the files via the php endpoints, and those endpoints can perform validation of the user's credentials.
I have solved it by doing following:
Added a .htaccess file in my folder htdocs/uploads with following content:
<FilesMatch ".*">
Order Allow,Deny
Deny from All
</FilesMatch>
Created a file: file_download.php with following content:
// Doing here some user verification based on the session and user_id
if($user['id'] != 'something i want to be') {
die('Not allowed to download this file');
} else {
// Verify the download as requested
// Basic file name
$file = basename($_GET['file']);
// Path where the file should be stored
$file = 'uploads/'.$file;
if(!file_exists($file)){ // file does not exist
die(''.$file.' file not found');
} else {
header("Cache-Control: public");
header("Content-Description: File Transfer");
header("Content-Disposition: attachment; filename=$file");
header("Content-Type: application/zip");
header("Content-Transfer-Encoding: binary");
// read the file from disk
readfile($file);
}
}
Changed the download link from <a href="' . $row['path'] . '" target="_blank" download>Download file</a> to <a href="file_download.php?file=<?= $row['path'] ?>Download</a>
This way is blocking any try to download a file by using clear URL in the browser. the file can only be downloaded by having a user identification and the provided link from my website. If someone does not need the user verification, just delete the first if/else in the file_download.php.
On a godaddy hosted website using CPanel, I have a small PHP script that shows each line in a text file that's on the server. Each line contains a private href link to a PDF that only the logged-in user can see. The links points to various PDFs in the same folder on the server. The code works fine and I can click on the link and see each PDF.
The problem is that each PDF can also be seen by using a direct URL query (i.e. website/folder/pdfname.pdf). As these are private PDFs, I don't want them public. I've tried changing CPanel permissions on the folder to "owner" - but that seems to prevent the PHP script from opening the PDFs also.
Is there a way to allow a PHP script access to PDFs in a folder - but prevent direct URL references?
NOTE: I'm not particularly adept at PHP or CPanel - sorry.
Code...
$fname = "PDF-" . $user_name.".txt";
$fnum = fopen($fname,"r");
echo "<tr>";
While (($str = fgets($fnum)) !== false) {
$arr = explode("|",$str);
for ($x = 0 ; $x < count($arr); $x++) {
echo "<td>$arr[$x]</td>";
}
echo "</tr>";
}
echo "</tr>";
fclose($fnum);
File contents...
Xyz Company|21 Jan 2018| website link
Xyz Company|21 Jan 2018| website link
Xyz Company|21 Jan 2018| website link
Xyz Company|21 Jan 2018| website link*
Asside from removing the files from the root, if you are running apache, you can change your .htaccess (I'm sure windows-based system have a web.config equivalent) to forbid access to certain files directly. If you add this snippet to that file, it will deny files with .pdf extension:
<FilesMatch "\.(pdf)$">
Order Allow,Deny
Deny from all
</FilesMatch>
From there, inside your app, you can create some sort of system for curating your PDF links, so if you store the real path in a database and use the id as the link similar to:
http://www.example.com/?file=1
or if you just do a simple scan:
<?php
# The folder that the PDFs are in
$dir = __DIR__.'/website/folder/';
# Loop over a scan of the directory (you can also use glob() here)
foreach(scandir($dir) as $file):
# If file, create a link
if(is_file($dir.$file)): ?>
<?php echo $file ?>
<?php
endif;
endforeach;
Then, if the user tries to download using the link, you check they are first logged in and if they are, download the file by doing a script like so BEFORE you output anything else to the browser (including spaces):
<?php
session_start();
# First check that the user is logged in
if(empty($_SESSION['username']))
die('You must be logged in to download this document.');
# Not sure which directory you are currently in, so I will assume root
# I would do basename() here incase the user tries to add in something like:
# ../index.php and tries to download files they are not supposed to
$file = __DIR__.'/website/folder/'.basename($_GET['file']);
if(!is_file($file))
die('File does not exist.');
# Double check that the file is a pdf
elseif(strtolower(pathinfo($file, PATHINFO_EXTENSION)) != 'pdf')
die('File appears to be invalid.');
# Start download headers
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;
One simpler and basic example (and derivative of previous answer) is to use two separate PHP files, where one is evaluating a set cookie (set to expire soon) in the browser upon link click (set via JS or PHP or other). If the cookie was read correctly, the first PHP page imports a second page that utilizes the PHP header() redirect containing your original file name forcibly downloaded with another name. Using the Content Disposition header field.
In action this works like this
1: Original page with download links - we set the cookie to work for 2 minutes
<a onclick="setCookie(1, 1, 2, 60)" href="php-secure-files-delivery-page.php">Download My Final PDF name.pdf</a>
<script type="text/javascript">
// set a cookie with your own time limits.
function setCookie(days, hours, minutes, seconds) { // Create cookie
var expires;
var date = new Date();
date.setTime(date.getTime()+(days*hours*minutes*seconds*1000));
expires = "; expires="+date.toGMTString();
document.cookie = "my_cookie_name"+"="+"my_cookie_value"+expires+"; path=/";
}
</script>
On the link page we include a hyperlink with the evaluating PHP page. Here we use JavaScript to set a cookie using the custom function setCookie(days, hours, minutes, seconds), that will receive your wishes for expiry. Just note that 1 is the minimum number. Not 0.
2: Download page - evaluating cookie and presenting texts, or simply downloading the file
(php-secure-files-delivery-page.php)
<?php
// if the cookie is set correctly, load the file downloader page.
if (isset($_COOKIE['my_cookie_name'] && $_COOKIE['my_cookie_name'] === 'my_cookie_value')) {
require_once 'file-downloader.php'; // the file will force the download upon import.
} else {
die('The link expired, go to your downloads section and click on the link again.');
}
?>
Here we evaluate the cookie, present either the correct info or die(). Using require_once we get the PHP page into the current one.
3: Imported file includer PHP page
(file-downloader.php)
<?php
// We'll be outputting a PDF
header('Content-Type: application/pdf');
// It will be downloaded as your-downloaded.pdf
header('Content-Disposition: attachment; filename="your-downloaded.pdf"');
// The PDF source is in your own specified long name
readfile('original-with-really-weird-original-name.pdf');
?>
Results
User always go to the same page, being presented with the appropriate information.
You can name your original files on your server anything you want, like "my_really_difficult_and_long_file_name.pdf", while the user sees only the nice pretty file name when the file is downloaded.
for more files, use an extra input in the cookie function to take the file name too, and some if statements in the php downloader page, that looks for separate end PHP pages to require_once.
If you go to the browsers "Downloads" section to try to get the url of the downloaded file, you see the initiating PHP page, the second page, that leaves you empty with a die() if no correct cookie was set. That cookie is only set when you want it to. On your pages. You can of course do this in JavaScript too, but that will expose the cookie, still, for most unauthorized sharing, that takes care of it.
Lastly, easy security for your folder (without Apache/Nginx/.htaccess stuff)
Using .htaccess files on local folders or directives on your server is the best and most secure way. But that´s not transferable to your other applications on other systems. Instead use a index.php and a default.php page on your PDF file´s parent folder, where they are located, including this header redirect to wear off unwanted visits:
<?php
header("Location: http://yoursite.com/some-other-page/"); /* Redirect browser here */
?>
I face a case I never did, and I dont know how to properly do it.
I have a php script which generate files for clients. At the end of the script, I echo the path for them to download the file, simply.
How can I do to provide the file - or the path or any what - for downloading it, and be sure to delete the file once downloaded.
Widely, I'd like to make the file available for one/unique download only. How to ?
EDIT
I cannot use headers
There are a few components to getting this to work. Without knowing which framework you use, I'll use comments as placeholders.
There is no way to do it without using the header function, though.
Here is the source for a file that outlines the process:
<?php
$fileid = $_GET['fileid'];
$key = $_GET['key'];
// find the file in the database, and store it in $file
if ($keyMatches) {
// it is important for security to only use file paths from the database
$actualPath = $file->getPathOnDisk();
$fileInfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_file($fileInfo, $actualPath);
$fp = fopen($actualPath, 'rb');
header("Content-Type: " . $mime);
header("Content-Length: " . filesize($actualPath));
fpassthru($fp);
}
else
{
http_response_code(403); // forbidden
}
You'll use this by linking to download.php?fileid=1234&key=foobar, and generating the URL at the same time you generate the key and store it in the database.
For security, you'll keep the files outside of the web root, meaning they cannot be accessed through the web server without going through a script.
fpassthru is reasonably fast, and will not likely have a performance impact.
You must do a download file gateway, like download.php?id=XXX
Where XXX is the unique ID of each file you will store in DB. And of course, the file to be downloaded.
Then, each time a user will visit the page, you can :
- Check if he has already downloaded the file
- If no, redirect it to the real path of file
- If yes, display 403 message.
When a user download a file, update the DB, generate or copy the file to a new name, you play with headers, and delete file upon download or after a small timeout.
I want to protect a pdf file from being directly linked but instead have my logged in users be able to access it. I have a link which currently goes to a javascript function which posts a form:
$('nameofdoc').setProperty('value',doc);
document.getElementById('sendme').submit();
where sendme is the name of the form and nameof doc the index of the document I want to display.
This then goes to a php file:
$docpath = $holdingArray[0].$holdingArray[1];
$file = $holdingArray[0]; //file name
$filename = $holdingArray[1]; //path to the file]
header( 'Location:'.$docpath ) ;
header('Content-type: application/pdf');
header('Content-Disposition: attachment; filename="'.$filename . '"');
readfile($filename)
This all works fine it loads up the file and outputs the pdf. What I can't do is protect the directory from direct linking - ie www.mydomain.com/pathToPdf/pdfname.pdf
I've thought of using .htaccess to protect the directory but it's on a shared host so I'm not sure about the security and anyway when I've tried I can't get it to work.
Any help would be great since this is my fourth day of trying to fix this.
thanks
Update
I've had a lot of help thank you but I'm not quite there yet.
I've got an .htaccess file that now launches another php file when a pdf is requested from the directory:
RewriteEngine on
RewriteRule ^(.*).(pdf)$ fileopen.php
When the fileopen.php file lauches it fails to open the pdf
$path = $_SERVER['REQUEST_URI'];
$paths = explode('/', $path);
$lastIndex = count($paths) - 1;
$fileName = $paths[$lastIndex];
$file = basename($path);
$filepath = $path;
if (file_exists($file)) {
header( 'Location: http://www.mydomain.com'.$path ) ;
header("Content-type: application/pdf");
header("Content-Disposition: attachment; filename=".$file);
readfile($filepath);
}else{
echo "file not found using path ".$path." and file is ".$file;
}
The output is
file not found using path /documents/6/Doc1.pdf and file is Doc1.pdf
but the file does exist and is in that direcotry - any ideas??
OKAY I'm happy to report that Jaroslav really helped me sort out the issue. His method works well but it is tricky to get all the directory stuff lined up. In the end I spent a few hours playing about with combinations to get it working but the principle he gave works well. Thanks
The best way would be to protect that folder with htaccess, as you have mentioned. So you put all PDFs in pdf/ folder, and in the same pdf folder you out .htaccess file:
RewriteEngine on
RewriteRule .* your-php-script.php
Now no files can be accessed by url in this folder. Every request to every file in this folder will return what your-php-script.php script returns. In your-php-script.php you do something like this:
//Check if user has right to access the file. If no, show access denied and exit the script.
$path = $_SERVER['REQUEST_URI'];
$paths = explode('/', path);
$lastIndex = count($paths) - 1;
$fileName = $paths[$lastIndex]; // Maybe add some code to detect subfolder if you have them
// Check if that file exists, if no show some error message
// Output headers here
readfile($filename);
Now if user opens domain.com/pdf/nsa-secrets.pdf Apache will run your-php-script.php. Script will have variable $_SERVER['REQUEST_URI'] set to "domain.com/pdf/nsa-secrets.pdf". You take the last part (filename) and output it to a user (or not).
This will stop anyone from accessing files directly from the internet by knowing URL. If someone has direct access to files on your server, that will not stop them. On the other hand, I think any shared hosting stops users from getting files of other clients. Only way to do it is to hack the server in some way. But then we are getting very paranoid and if that may be a case for you, you shouldn't use shared hosting in the first place.
If you cannot make htaccess work, you can try to obfuscate files, so it would be difficult to spot them for someone outside. For example change file from mySecretData.pdf to djjsdmdkjeksm.pdf. This may help a little bit.
I want to protect a pdf file from being directly linked but instead have my logged in users be able to access it.
Check to ensure there is an authenticated user before streaming the PDF's content.
This is kinda sloppy but it could work assuming you can setup a MYSQL DB. It lets you pass the "password" in the URL as an MD5 string or as a clear text if you want to. Trying to setup some kind of security without using htaccess or an existing frame work is kinda clunky. This however won't even attach the file to the stream until it knows you've been "Authenticated" I think you could maybe make this a little better if you setup a login page that saved a cookie locally then you wouldn't need to pass the "passphrase" in the URL.
$file = $_GET['file'];
$pass = $_GET['pass'];
$download_folder = '../Protected';
$file = basename($file);
$filepath = "$download_folder/$file";
if (file_exists($filepath)) {
if(CheckUser($pass)){
header("Content-type: application/octet-stream");
header("Content-Disposition: attachment; filename=$file");
session_write_close();
readfile($filepath);
} else {
echo 'Not Authenticated!';
}
} else {
echo 'No File!';
}
function CheckUser($value){
$con = mysqli_connect("test.com","test","123456","my_db");
// Check connection
if (mysqli_connect_errno()){
echo "Failed to connect to MySQL: " . mysqli_connect_error();
}
$result = mysqli_query($con,"SELECT user FROM pass_table WHERE password =".md5($value).";");
while($row = mysqli_fetch_array($result)){
mysqli_close($con);
//return $row['user'];
if($row['user']){
return true;
}
}
mysqli_close($con);
return false;
}
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.