2

I have an issue that seems like a very common requirement, but I'm unable to find any help. Let's say I have an authenticated user uploading private photos to non browsable folder on my server. Each user has their own folder in a large file store, like...

/FileStore/{UserId}/Photos/my_cute_cat.jpg

The file is uploaded and I save a thumbnail of the photo like...

/FileStore/{UserId}/Photos/Thumbs/my_cute_cat_thumb.jpg

That user wants to download their photo. No problem...

  • User sends a request to download
  • I authorize the user and make sure they own that particular photo
  • I serve the file

I need display the thumbnail in a plain old img tag on the user's dashboard. The /Thumbs/ folder is not set up to serve static images. I don't want the /thumbs/ folder to be able to serve static images because they should only be visible to authorized users. What am I supposed to do?

  • 1
    Feed the thumbnails through an Authorised Controller action. Something similar to answer for this question: https://stackoverflow.com/questions/186062/can-an-asp-net-mvc-controller-return-an-image Obviously you will tweak the security but this would be the general idea. – Heinrich Feb 02 '18 at 03:47
  • 1
    The idea is pretty simple. Whenever you want to serve a file that requires authorization, you need to serve it via a FileActionResult while decorating the action method with [Authorize] attribute. Static folders are publicly available, i.e. they are on the `wwwroot` folder. They are security vulnerable. – Fiyaz Hasan Feb 02 '18 at 06:57

3 Answers3

4

If its just a small thumb nail, consider using embedded base64 image with more details here: How to display Base64 images in HTML?

You can pass base64 down to the View by encoding the file into a base 64 format as a string explained here: http://www.devcurry.com/2009/01/convert-string-to-base64-and-base64-to.html

Using this approach or even using a FileActionResult to serve the file through a controller has the big disadvantage of not being able to use a CDN to deliver the cached content. What you can do to help with this is still serve the images statically but give them obscenely long random names which is unguessable. When someone requests the image from you, then you simply provide them with the unguessable url.

Mitch Dart
  • 1,142
  • 1
  • 11
  • 32
  • How does this solve the security concern? Serving the image really isn't the problem for the OP. – Chris Pratt Feb 02 '18 at 14:39
  • @ChrisPratt I don't understand how you don't see how it solves the problem? Since his security sits on his controller layer, he wants to make business decision on the controller to decide if the user can see the image or not. Since he is serving it statically, he cannot achieve it. I gave one example using base64 to serve it through the controller layer, and another example to serve it statically (the same way facebook secures their images) by making the url unguessable. This is the preferred way in industry. – Mitch Dart Feb 03 '18 at 08:48
  • @ChrisPratt The only reason why I mentioned the base64 method is because OP mentioned that he is serving a authorised page with just an tag already. If OP puts the base64 encoded image on that page directly instead of referencing a static location, then it already solves his problem without much effort. – Mitch Dart Feb 03 '18 at 08:58
  • I like this approach. I really don't want to use a controller action to load what may be 20 or 30 images, while having to verify access authority from my database (or set up some caching). – dweeb_plus_plus Feb 06 '18 at 01:22
2

First and foremost, if a static file should not be available to the whole world, then your web server should not serve the directory it is in at all. Nothing else will do on that front. If the directory is served, then the image can leak.

That then presents the problem of how to allow the user to access it. The simple answer there is that you need an authorized action that returns the image(s). Basically, that's going to look something like:

[Authorize]
public async IActionResult Image(string image)
{
    var file = $"/FileStore/{User.Identity.GetUserId()}/Photos/{image}";
    if (!File.Exists(file))
        return NotFound();

    return File(file);
}

Now, obviously, I don't know about your "FileStore", so the exact code here may need to change. The general idea is that you simply see if this file exists under the user's directory. If it does, they can have it. If not, they don't own it. You also should probably add in a bit more security, such as restricting the image param to only image types, so someone can't try to pull just any arbitrary file. They'd still have to somehow manage to get some aberrant file there, in the first place, but just in case it's better to not take chances.

The return methodology may need to change as well. Right now, I'm assuming a file on the filesystem of the server, so I just return a FileResult with the path. If you're pulling it from some sort of storage account in Azure, AWS, etc. then you'd use HttpClient to make the request, and then you should stream the response from that directly to a FileStreamResult.

Chris Pratt
  • 207,690
  • 31
  • 326
  • 382
  • This means that all that expensive image downloading traffic needs to come from his server? Since most people use a CDN to deliver images on edge servers, this is very bad practice. Check how facebook secures it using access tokens in image urls. https://www.quora.com/Are-Facebook-pictures-really-private-or-are-they-hosted-on-Facebook-servers – Mitch Dart Feb 03 '18 at 09:05
1

I've not tested on linux, but if you make a folder where you have you pictures, you can actually just create a controller method that returns a file. Now, even if you are using a view served from another method, you can still call this file method from this razor view and display the image.

in controller called App I serve the image from a folder called Documents:

public IActionResult File(string id)
    {        
        if (String.IsNullOrEmpty(id))
        {
            return PhysicalFile(Path.Combine(_env.ContentRootPath, "Documents", "Wrong.png"), "image/jpg");
        }

        return PhysicalFile(Path.Combine(_env.ContentRootPath, "Documents", id), "image/jpg");
    }

in razor (using a bit bootstrap):

 <img src="~/App/File/@profilePicture" class="img-fluid" />

use the location of your method that serves the file.

Johan Herstad
  • 594
  • 5
  • 15