[Django]-Restricting access to private file downloads in Django

28👍

Unfortuanately @Mikko’s solution cannot actually work on a production environment since django is not designed to serve files. In a production environment files need to be served by your HTTP server (e.g apache, nginx etc) and not by your application/django server (e.g uwsgi, gunicorn, mod_wsgi etc).

That’s why restricting file acccess is not very easy: You need a way for your HTTP server to ask the application server if it is ok to serve a file to a specific user requesting it. As you can understand thiss requires modification to both your application and your http server.

The best solution to the above problem is django-sendfile (https://github.com/johnsensible/django-sendfile) which uses the X-SendFile mechanism to implement the above. I’m copying from the project’s description:

This is a wrapper around web-server specific methods for sending files to web clients. This is useful when Django needs to check permissions associated files, but does not want to serve the actual bytes of the file itself. i.e. as serving large files is not what Django is made for.

To understand more about the senfile mechanism, please read this answer: Django – Understanding X-Sendfile

2018 Update: Please notice that django-sendfile does not seem to be maintained anymore; probably it should still be working however if you want a more modern package with similar functionality take a look at https://github.com/edoburu/django-private-storage as commenter @surfer190 proposes. Especially make sure that you implement the "Optimizing large file transfers" section; you actuallyu need this for all transfers not only for large files.

2021 Update: I’m returning to this answer to point out that although it hasn’t been updated for like 4 years, the django-sendfile project still works great with the current Django version (3.2) and I’m actually using it for all my projects that require that particular functionality! There is also an actively-maintained fork now, django-sendfile2, which has improved Python 3 support and more extensive documentation.

3👍

If you need just moderate security, my approach would be the following:

1) When the user uploads the file, generate a hard to guess path for it. For example you can create a folder with a randomly generated name for each uploaded file in your /static folder. You can do this pretty simply using this sample code:

file_path = "/static/" + os.urandom(32).encode('hex') + "/" + file_name

In this way it will be very hard to guess where other users’ files are stored.

2) In the database link the owner to the file. An example schema can be:

uploads(id, user_id, file_path)

3) Use a property for your FileFields in the model to restrict access to the file, in this way:

class YourModel(models.Model)
    _secret_file = models.FileField()

    def get_secret_file(self):
        # check in db if the user owns the file
        if True:
            return self._secret_file
        elif:
            return None # or something meaningful depanding on your app

    secret_file = property(get_secret_file)
👤sc3w

2👍

This is best handled by the server, e.g. nginx secure link module (nginx must be compiled with --with-http_secure_link_module)

Example from the documentation:

location /some-url/ {
    secure_link $arg_md5,$arg_expires;
    secure_link_md5 "$secure_link_expires$uri$remote_addr some-secret";

    if ($secure_link = "") {
        return 403;
    }

    if ($secure_link = "0") {
        return 410;
    }

    if ($secure_link = "1") {
        // authorised...
    }
}

The file would be accessed like:

/some-url/some-file?md5=_e4Nc3iduzkWRm01TBBNYw&expires=2147483647

(This would be both time-limited and bound to the user at that IP address).

Generating the token to pass to the user would use something like:

echo -n 'timestamp/some-url/some-file127.0.0.1 some-secret' | \
openssl md5 -binary | openssl base64 | tr +/ -_ | tr -d =

1👍

Generally, you do not route private files through normal static file serving directly through Apache, Nginx or whatever web server you are using. Instead write a custom Django view which handles the permission checking and then returns the file as streaming download.

  • Make sure files are in a special private folder folder and not exposed through Django’s MEDIA_URL or STATIC_URL

  • Write a view which will

    • Check that the user has access to the file in your view logic

    • Open the file with Python’s open()

    • Return HTTP response which gets the file’s handle as the parameter http.HttpResponse(_file, content_type="text/plain")

For example see download() here.

0👍

For those who use Nginx as a webserver to serve the file, the ‘X-Accel-Redirect’ is a good choice.

At the first, request for access to the file comes to Django and after authentication and authorization, it redirects internally to Nginx with ‘X-Accel-Redirect’. more about this header: X-Accel-Redirect

The request comes to Django and will be checked like below:

if user_has_right_permission
    response = HttpResponse()
    # Let nginx guess to correct file mime-type by setting
    # below header empty. otherwise all the files served as
    # plain text
    response['Content-Type'] = ''
    response['X-Accel-Redirect'] = path_to_file
    return response
else:
    raise PermissionDenied()

If the user has the right permission, it redirects to Nginx to serve the file.

The Nginx config is like this:


server {
    listen 81;
    listen [::]:81;
    
    ...
    location /media/ {
        internal; can be accessed only internally
        alias /app/media/;
    }
    ...
}
Note: The thing about the path_to_file is that it should be started with "/media/" to serve by Nginx (is clear though)

Leave a comment