huconglobal's avatar

How to secure assets (images and docs)

I need to protect images and documents from public access (i.e. they cannot be in the public-folder). This means that Laravel has to authenticate the user that issued the request, and then somehow serve the image / document.

Maybe I haven't been using the correct search-words for this, but I've found frustratingly little about how to do this in a good way. I find that a bit strange, as this, to me, seems like quite a common use case. In any case, I found that the most common proposition is to use PHP-functions like readfile() or fopen() to load the image and then return it as the response (with the correct Content-Type). This is woefully inefficient, as the file requested has to be read into memory before being returned.

The second solution I've found, is to leverage functionality on the webserver. Apache has something called X-Sendfile, the Nginx equivalent of which is X-Accel-Redirect. As I've understood it, this allows you to define a protected directory in the server config files (for Nginx, at least). The webserver can then read and serve files from these locations, but they aren't publicly accessible.

Are there any thoughts on this in the community? Has anyone got any experience with the latter solution (this seems to be the most viable one)?

Thanks!

0 likes
9 replies
RemiC's avatar
RemiC
Best Answer
Level 8

In Laravel, basically you just put your file outside the public folder (eg:/storage), and use the Response::download() method to send the file to the client.

Internally this method uses the Symfony\HttpFoundation component, which use binary streaming to transfer the file, so AFAIK at no point a large file would be loaded into memory.

7 likes
alexwolff's avatar

I personally created a route for that.

I use it to serve images. In addition to that I use http://image.intervention.io/ to server the images.

Route:

$this->get('/images/files/{file}','MediaServerController@serveImages')
            ->where(['file'=>'.*']);

Controller

class MediaServerController extends Controller{

    public function serveImages ($file)
    {
    $storagePath = storage_path('app/images/'.$file);
     if( ! \File::exists($storagePath)
        return view('errorpages.404');

        return \Image::make($storagePath)->response();
    }
}

Basically what this does is to take

The Url http://example.com/images/files/image.jpg

The controller checks if the file (Image.jpg) exists in the storage (app/images/image.jpg) folder if the intervention image library renders the images and creates a response.

It will work for any depth of subfolders as well for ex.

http://example.com/images/files/subfolder/image.jpg

Now the controller will look into your subfolder (app/images/subfolder/image.jpg) in your images folder.

For other files than Images you could use the Response::download()

Hope that helps

Now, you can secure it how ever you want with a middleware on the route / controller for instance.

Down side of this approach is that yo have to boot your application every time you access an image. The response will take much longer compared to the response from your web server (apache/nginx) who would serve the image normally for you if they were in the public folder.

You have to make this trade of security vs performance.

Alex

1 like
kingpabel's avatar

You can encoding the image/docs in base64 format and keep the encoding code in your database.So it will more secure.Then you can show or send that image/docs as per you want.For encoding base64 method and display that encoding to image/docs follow this url. http://kingpabel.com/php-base64_encode/

jhuliano's avatar

@kingpabel base64 have nothing to do with encryption, it's just an encoding (just like UTF-8). Also do you have any idea how a 5-10mb file would look like in base64? It's a pretty huge string to be stored in a DB, it doesn't seems like a good idea!

edgreenberg's avatar

I needed a more general case, for files of all sorts, and I didn't want to involve image.intervention.io. I place the files in storage/files Here is my route and controller:

Route:

Route::get('files/{file}', 'StaticFileController@serveFile')->middleware('auth');;

Here is my controller:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
Use Response;

use App\Http\Requests;

class StaticFileController extends Controller
{
    public function serveFile ($file)
    {

        $storagePath = storage_path('files/'.$file);
        $mimeType = mime_content_type($storagePath);
        if( ! \File::exists($storagePath)){
            return view('errorpages.404');
        }
        $headers = array(
            'Content-Type' => $mimeType,
            'Content-Disposition' => 'inline; filename="'.$file.'"'
        );
        return Response::make(file_get_contents($storagePath), 200, $headers);

    }
}

Note the line; Use Response; This is important.

3 likes
huconglobal's avatar

@edgreenberg Your solution looks a lot like the one I ended up with, the difference being that I use the database to help manage my users' files, so File is actually a model:

public function serve(File $file)
{
    $path = storage_path('files/'.$file->url);
    $mime = File::mimeType($path);

    return response(
        File::get($path), 200, [
            'Content-Type' => $mime,
            'Content-Disposition' => 'inline; '.$file->url
        ]
    );
}

P.S. As you can see, you can use the response()-function instead of the Response-facade. Then you don't have to include 'use Response'.

Also, I would recommend that you return an actual 404-response instead of just the 404 errorview, which will have a HTTP status-code of 200.

if ( ! \File::exists($storagePath) )
{
    abort(404);
}

If you keep your 404-errorview here: resources/views/errors/404.blade.php, it should be displayed when you call abort(404).

edgreenberg's avatar

@effkay: Thanks for writing in. I will be reviewing your additions and updating my code (tomorrow, not tonight :))

In my case, I want the website owner to be able to drop files into the /storage/files directory without paying me contractor rates to do what he can do himself.

If the solution is improved further, I'll post back.

Best,

Ed

Please or to participate in this conversation.