How can I restrict downloading files through PHP authentication?

Asked

Viewed 1,281 times

3

I have a system where the user can upload audios. The user level that can do this is the client. But on the other hand, I have the people who can download these files to do the audio transcription. Only authenticated persons (and their respective levels) can have access to these files.

The problem is that we usually upload files to a public folder, where you can access the download link. However, in that case, I don’t want that to happen, because someone who lost access to the system could have access to that link if someone else passed by.

I wonder if there is any way to restrict access to file downloads by authentication.

I just need an idea to know how to do it in a way that only those who are authenticated have access to downloads.

Observing: The answer doesn’t even have to have code, but just ideas of how I do.

  • 1

    Are you using any framework? I just went through it and was using Windows. The basic logic is to remove from public (of course) and "stream" the file in an authenticated route.

  • @Rafaelmenabarreto good idea. If you want to add this as a response. And yes, I use Laravel. If you have any way to add an answer to someone who uses "pure PHP", you’ll be even better

  • OK I’m creating an answer

  • 1

    Duplicate of the previous one? ;) http://answall.com/questions/128049/70

  • Seems so.

3 answers

6


You can try the simple medium where PHP reads the entire file and dispatches it to the user. However, this technique has a high cost because it consumes memory, and the larger the file, the greater the consumption.
We can consider large files from 1mb. And this is proportional to the volume of accesses. In a very busy site for example, 10kb is also heavy. Anyway.

One feasible option is to use in conjunction with the module X-Sendfile.

If you can install X-Sendfile on the server, it is a better option regarding the use of functions such as file_get_contents(), readfile() and fopen().

The logic in using X-Sendfile is that a "route deviation" is made during PHP processing where the web server itself (apache, Nginx, etc.) will dispatch this file to the user, without using PHP to read the file.

inserir a descrição da imagem aqui

In the image above, the traditional process using only PHP.

User requests
Webserver sends data to PHP
PHP processes (does authentication and anything else) Then read from it.
PHP writes the contents of the file to the webserver
The webserver dispatches to the user

See the same situation with X-Sendfile

inserir a descrição da imagem aqui

User requests
Webserver sends data to PHP
PHP processes (does authentication and anything else) Then send a header to the webserver (PHP work ends here).
webserver understands the header and invokes the X-Sendfile module that returns the path of the file to be dispatched.

Note: X-Sendfile does not read the contents of the file. Just let the webserver know that the content to be dispatched is in the path specified in the header provided by PHP.

Practical example

It’s not a complete example of an authenticated download as the question asks, but it’s a useful example of how to use X-Sendfile.

Create a folder on your localhost or wherever you prefer:

/xsendfile

Inside that folder create a folder imgx and an archive index.html with the following content:

ok, imagem<br>
<img src="img/1.jpg">

Remember that we previously created the folder imgx but in HTML we are calling img/ that doesn’t exist.

The structure is like this

/xsendfile
    /index.html
    /imgx/1.jpg

The archive 1.jpg is any image for testing. It must be inside the folder imgx. This folder will be accessible by X-Sendfile.

Create a file .htaccess with the following content:

XSendFile on

RewriteEngine On
RewriteRule ^/?img/(.*)$ /xsendfile/img.php?file=$1 [L,R=301]

Save it in the folder /xsendfile

Now the structure is like this:

/xsendfile
    /.htaccess
    /index.html
    /imgx/1.jpg

Let’s create the router. In htaccess is img.php.

That is the code:

<?php
date_default_timezone_set('Asia/Tokyo');

ini_set('error_reporting', E_ALL & ~E_STRICT & ~E_DEPRECATED); // & ~E_NOTICE
ini_set('log_errors', true);
ini_set('html_errors', false);
ini_set('display_errors', true);

/*
The images must be on the same directory or into subfolders.
Not allowed to load images from up folders.

Must turn it on into htaccess file:
    XSendFile on
*/

/*
true: send file headers
false: do not send the file headers
*/
define('FILE_HEADERS_ENABLED', false);

if (isset($_GET['file'])) {
    $file = $_GET['file'];
}

// File's absolute path
/*
Lembra da pasta "imgx"? É aqui que setamos o caminho real. 
Lembre-se que o nome da pasta deve ser dificil de ser advinhada caso esteja num diretório público. Mais para frente explico porque nesse exemplo colocamos numa pasta pública.
*/
$path = __DIR__.DIRECTORY_SEPARATOR.'imgx'.DIRECTORY_SEPARATOR.$file;

// Checking if path exists.
// Se o arquivo não existir, carregará uma imagem padrão.
if (!file_exists($path)) {
    // Path not found. Will load default missing image.
    $path = __DIR__.DIRECTORY_SEPARATOR.'imgx'.DIRECTORY_SEPARATOR.'no_image.gif';    
}


/* 
Aqui ou em qualquer outra parte que for conveniente para o seu caso,
poderia chamar rotinas de autenticação, por exemplo.
*/
$autentica_algo = true;
if ($autentica_algo === false) {
    // aqui não autenticou, então interrompe, envia um arquivo falso ou sei lá.. Cada um inventa a firula que quiser.
}


/*
Os headers abaixo pode apagar se quiser. Não faz diferença. Mas dependendo do caso pode ser útil.
Normalmente utilizo para testes.
*/
header("Cache-Control: no-cache, must-revalidate"); //HTTP 1.1
header("Pragma: no-cache"); //HTTP 1.0
header("Expires: Sat, 26 Jul 1997 05:00:00 GMT"); // Date in the past

if (FILE_HEADERS_ENABLED) {
    $info = getimagesize($path);
    header('Content-type: '.$info['mime']);
    header('Content-length: '.filesize($path));
    header('Content-Disposition: inline; filename="'.basename($file).'"');
}

// Sent the x-sendfile header
// Esse header invoca o módulo X-Sendfile.
header('X-Sendfile: '.$path);

/*
Aqui pode usar um exit se preferir que o script não continue. Mas normalmente já interrompe no header acima.
Por via das dúvidas, interrompa explicitamente.
*/
exit;

With all this we have the structure:

/xsendfile
    /.htaccess
    /index.html
    /img.php
    /imgx/1.jpg

Run and see what happens: http://localhost/xsendfile/

The image in the folder will be displayed imgx even though there is no place img/1.jpg.

Try removing or disabling . htaccess and see what happens. Returns a not found in the image as there is no routing and therefore will not load the image invoked by X-Sendfile.

You can also try directly in the browser http://localhost/xsendfile/img/1.jpg because htaccess takes care of the redirect and the img.php will invoke the X-Sendfile.

How to install X-Sendfile?

To use X-Sendfile you usually need administrative access to the server (dedicated, vps). Usually in shared hosting there is no module installed and hardly the support of the provider would make the installation. Therefore this solution is usually more accessible to those who have administrative access to the server environment.

*Considering Apache as webserver

For Windows: https://www.apachelounge.com/download/ or https://tn123.org/mod_xsendfile/

For Linux: https://tn123.org/mod_xsendfile/

Additional explanation of the structure of the example

To better understand by following the example above:

/xsendfile
    /.htaccess
    /index.html
    /img.php
    /imgx/1.jpg

To further protect the files in the folder imgx/, we could put it out of public folder.

c:/ww/private/imgx/1.jpg
c:/ww/public/xsendfile
    /.htaccess
    /index.html
    /img.php

This would be ideal, but X-Sendfile does not allow directory indentation for security reasons. It is possible to apply this way by making some implementations but, to avoid complicating the example, I demonstrated in a simpler way.

The module is open source and anyone can recompile and remove such restrictions. But only do so if you know what you are doing.

Remarks on the use of X-Sendfile

  1. It usually conflicts with URL rewriting rules. If you have rewrite rules on an already working system, prefer to use the X-Sendfile separate from that system. Unless you’re a Mazoquist and want to do tricks to make it compatible.

  2. Cannot upload to X-Sendfile header, files above the directory from which it is invoked. That is, it is not allowed to define a path with indentation of directories.

  3. The module is developed by a third party without support from a company that guarantees continuity. This is perhaps the weakest point because the day the developer fails to maintain, all who use it will be without updates and support or will have to learn to read the code and give continuity to the project. Anyway, it depends on the Velopers community.

  • Daniel, this one I didn’t know. Surprising. I’m going to do a little research here :D

  • But in case you use readfile instead of using file_get_contents would no longer be "economic"?

  • also gives in it.. file_get_contents, readfile, fopen, etc. even using stream techniques.. the problem is that it is reading the entire file to then write it and all this inside the server.. which in the end dispatches to the user. The ideal is for the server to dispatch directly if it needs to read anything.

  • @Wallacemaxters no way, making withfile_ is much more "heavy". X-Sendfile returns control to the page server, and escapes the entire PHP buffer. It is one less layer, and still saves the "manual" cache control, because taking the path, the page server does practically as if it were a static file. In Apache is a separate module.

  • @Bacco you’re alive, right! I’ll take a look at this "stop" now :D

  • @Wallacemaxters I just used for a thumbnails system. PHP generates the image if there is no thumbnail, and saves it in the folder. But X-Sendfile is the one that serves it from the cache folder. https://tn123.org/mod_xsendfile/ (and is given the +1 pro Omine by the answer)

  • rsrs.. I am very sleepy.. but I’m going to make a small effort to put more things.. It’s just a short time away..

  • +1 did not know this way and much less this module (:

  • Perhaps, to make it easier for OS users who come later to consult this topic, it is good to inform at the beginning of the answer that this alternative requires administrative access to server settings. And not just to the project files, as indicated in the question tags. Because many users with shared hosting will not be able to use this option even though it may be more efficient.

  • based on the above text "a high cost because it consumes memory" what would be the high cost? I have a download system and by tests I have noticed that when downloading a file without another has not finished destroys the download session. This could be one of the prices of memory consumption in the traditional way?

  • I can’t tell you the reason for your specific problem, @adrianosymphony. It might be a case of opening a question.

  • @Danielomine grateful for the answer. The question is already open at http://answall.com/questions/157096/fun%C3%A7%C3%A3o-readfile-debugging-sess%C3%A3o-php

Show 7 more comments

5

First, the downloadable file could not be publicly available.

Then you would need to create a logical route to the downloadable file directory. As an example, I will use /downloads/[arquivo].

In your http service (apache, Nginx), you would need to set the redirect to that route.


Example for Apache:

RewriteRule "^/downloads/.*$" "downloads.php"

Being downloads.php your script that will validate user authentication.


In the archive downloads.php you would need to verify that the user is authenticated, and forward it to the login page if negative, or send the file if it is already authenticated.


Example:

// Verifica autenticação
if (!isset($_SESSION['user_id'])) {
    header('location: /login.php');
    exit;
}

// Usuário autenticado

// Caminho do arquivo, assumindo que o diretório de download está em ../downloads
$path = '../' . parse_url($_SERVER['request_uri', PHP_URL_PATH);

if (!file_exists($path)) {
    // Arquivo não existe
    http_response_code(404);
    exit;
}


// Não foquei em pegar o file_mime e o file_name para encurtar a resposta e pois foge do escopo da resposta.
header('Content-Type: ' . $file_mime); 
header('Content-Disposition: attachment; filename="' . $file_name . '"');
readfile($path);
  • Very good, young man. Congratulations on the answer. Deserved +1

3

The logic behind this restriction consists in these steps:

  • Remove items from public directory
  • Create a path that receives the file as a parameter
  • On the authenticated route we open the file and start a stream of it

In pure PHP the user would access an address (example: 'http//www.meusite.com.br/.php files?name=file1.jpg') This route would do the normal check if the user is authenticated or not and return the image directly to him. Example using an image:

$file = fopen('localdoarquivo');
header("Content-type: image/jpeg");
echo $file;

This example is as "the sledgehammer as possible" note that u forced the image type to jpeg.

Using a framework like Laravel everything gets more simplified, you can use the Storage to find the file and the Intervention to return the image regardless of the extension:

try {
       $image = Storage::disk('seu-storage')->get($filename);
        } catch (\Illuminate\Contracts\Filesystem\FileNotFoundException $fl) {
            $response = new Response();
            $response->setStatusCode(404);
            $response->setContent(['success' => false, 'msg' => 'Arquivo não encontrado']);
            return $response;
        }

        return Image::make($image)->response();

As the question speaks in audio we changed the Return there to:

    $response = new Response($audio);

    return $response;

That the file (independent of the extension) will be sent to the user’s computer as a download.

  • 1

    That’s right. Laravel helps a lot. Those who still like to use pure PHP still have the first option. + 1

Browser other questions tagged

You are not signed in. Login or sign up in order to post.