0

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! :)

1 Answers1

3

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 '<a href="/download.php?file=' . $fileId . '">PDF Download</a>';

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.

Alex Howansky
  • 50,515
  • 8
  • 78
  • 98
  • Thanks for the reply Alex. I'm sort of already doing this, users login to the system and are presented with a menu which lists their bills and I have `` where the `$invoice_number` is the name of the file. – Milkyjoe Jan 14 '20 at 09:54
  • When you say 'put them outside the document root' where do you mean exactly; the web server directory structure is as follows... httpdocs/system/bills/ 'bills' has 0755 permissions, but all the pdf's have 0600. – Milkyjoe Jan 14 '20 at 10:00
  • I've changed this now so viewbill passes an id for the invoice row in the database table instead of the invoice number and added a check now so that the logged-in user id (stored in a session variable) is checked against the database table user for the said id. Also, there is no 'public' folder, it's just httpdocs/bills/ for example. – Milkyjoe Jan 14 '20 at 14:49
  • Also, I can't seem to open the pdf file in Adobe reader, I get a message about it not being a pdf or is corrupted, I can open it from the server so could is it not being encoded correctly?! – Milkyjoe Jan 14 '20 at 14:50
  • @Milkyjoe It doesn't matter what the actual name of the folder is, if it's under the document_root, then it's public and Apache will serve it. Somebody can just request `http://your.site/bills/12345.pdf` or `http://your.site/system/bills/12345.pdf` or whatever it is. – Alex Howansky Jan 14 '20 at 14:53
  • I know, that's why I've set the pdf permissions to 0600. I've tried to request a bill and get Forbidden. – Milkyjoe Jan 14 '20 at 15:09
  • If you've got perms set to 0600 then you won't be able to serve the file to anybody. – Alex Howansky Jan 14 '20 at 15:31
  • That's my problem. How do I make the files available, but also keep them secure?! At the moment I can serve the file using your example, but Adobe Reader says it's corrupted, but yet I can open the pdf in my web browser?! Works fine if opened directly from the server via FTP. – Milkyjoe Jan 14 '20 at 15:53