Secure File Download from a path outside of website root

Hmm, I should rethink my solution then. Security was my main reason to upload the files outside of the website root. The current solution from webassist does not post the path and the filename. It uses a php script to accomplish the download from the files outside of website root (my post from Jan. 13th). Any chance that you could implement a similar solution as webassist?

I asume you use MySQL. Here something I came up with, didnā€™t test it and you will need to adjust it to your needs.

<?php

$conn = new mysqli("localhost", "username", "password", "database");
if ($conn->connect_error) die("Connection failed: " . $conn->connect_error);

$stmt = $conn->prepare("SELECT DateiPfad, Dateiname FROM table WHERE DokumentId = ?");
$stmt->bind_param($_POST["DokumentId"]);
$stmt->execute();

$res = $stmt->get_result();
$row = $res->fetch_assoc();

$stmt->close();

$file = ('/usr/home/vewade' . $row['DateiPfad'] . '/' . $row['Dateiname']);

...

The above code is only an example on how to solve it, it will need some extra checks to see if the database returned a result set and return a 404 when record or file is not found. Check the PHP documentation at https://www.php.net/manual/en/mysqli.quickstart.prepared-statements.php, you should use prepared statements to provide protection against SQL injections.

Thanks a lot for your help.

I have the following code, but get a ERROR 500.

<?php

$conn = new mysqli("****.de", "***********", "********", "***********");
if ($conn->connect_error) die("Connection failed: " . $conn->connect_error);

$stmt = $conn->prepare("SELECT DateiPfad, Dateiname FROM ****** WHERE DokumentId = ?");
$stmt->bind_param($_POST["dokid"]);
$stmt->execute();

$res = $stmt->get_result();
$row = $res->fetch_assoc();

$stmt->close();

$file = ('/usr/home/vewade' . $row['DateiPfad'] . '/' . $row['Dateiname']);

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);
  } else {
    header("HTTP/1.0 404 Not Found");
}
?>

Any idea, what is missing?

Difficult to tell without the error message. In the link I gave you there are multiple samples of usage, it is possible that the prepare, execute or get_result failed. You should add extra checks for that.

if (!($stmt = $conn->prepare("SELECT DateiPfad, Dateiname FROM ****** WHERE DokumentId = ?"))) {
  die("Prepare failed: (" . $conn->errno . ") " . $conn->error);
}

if (!$stmt->execute()) {
  die("Execute failed: (" . $stmt->errno . ") " . $stmt->error);
}

if (!($res = $stmt->get_result())) {
  die("Getting result set failed: (" . $stmt->errno . ") " . $stmt->error);
}

etc...

It is

$row = $res->fetch_assoc() which causes the ERROR 500.

If I delete it I get

Execute failed: (2031) No data supplied for parameters in prepared statement

What does this mean? Sorry, but Iā€™m really not very good with PHP :thinking:

I found the error.

It was the bind_param. The correct statement is

$stmt->bind_param('s', $_POST["dokid"]);

This is my final code:

<?php
$conn = new mysqli("******.de", "********", "*******", "********");
if ($conn->connect_errno) {
    echo "Failed to connect to MySQL: (" . $conn->connect_errno . ") " . $conn->connect_error;
}

$stmt = $conn->prepare("SELECT DateiPfad, Dateiname FROM ******** WHERE DokumentId = ?");
$stmt->bind_param('s', $_POST["dokid"]);
$stmt->execute();

$res = $stmt->get_result();
$row = $res->fetch_assoc();

$stmt->close();

$file = ('/usr/home/vewade' . $row['DateiPfad'] . '/' . $row['Dateiname']);

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);
  } else {
    header("HTTP/1.0 404 Not Found");
}
?>

@George & @patrick,

What do you think? Is it secure enough now?

Thanks a lot for your help with this!

1 Like

Hi @patrick, @George & @Teodor,

is there any chance that this feature request will be integrated in Wappler?

Thanks!

1 Like

I have a similar need but am happy for files to be within the site root but the folder must not be browsable. I need to have files in there which are only accessed via logging into the site.

Can this be achieved just by adding a .htaccess into that folder? If so, how should I structure it?

To summariseā€¦

/files/docs
This contains files for logged in users only

Browsing to mydomain.com/files/docs must never show the contents even if they know the filename like mydomain.com/files/docs/doc1.docx

But a logged in person needs to be able to access the files which would be handled by Wappler-built pages.

Hope that makes sense.

A solution has been posted in this thread:

1 Like