Friday, December 25, 2015

XSendfile usage in Nginx

Suppose you have a corporate site or an internal portal or something else with access from web, but it's not a public site. And to gain access to any page or file your customer at least must be authenticated. It's not a problem for any web app, but it's a problem for static files (pdf, docx, xlsx and so on). With direct link anyone can download some sensitive information. And there are a lot of ways to share such links with the world unintentionally (mail, IM, web spiders, etc).

Old school way to protect a site from downloading direct links is to use http basic auth. It can solve a problem but It also creates some new problems. You need to manage one more database with users logins and passwords for basic auth and sync it with your app. Also http basic auth in web server is week against brute force attack. You can't use many tricks against brute force which are available for you in app. And don't forget about all your users pain because of double authentication process.

There is more elegant way to solve a problem with direct links. Nginx has XSendfile feature. You can use it for static files protection in such way:

  1. Nginx works as a frontend for your App.
  2. All links to static files send to App.
  3. If App thinks that user is authenticated it sends response with X-Accel-Redirect header with real path to static file, otherwise it sends response with some http 4xx error.
  4. In nginx vhost location for static files are marked with internal directive which protect files in it from direct download without X-Accel-Redirect header from upstream server.
Short example for Django App:
url(r'^app/static/(?P.*)$', StaticFile.as_view(), name="static_file")
 
class StaticFile(View):
    def get(self, request, path=None):
        if not request.user.is_authenticated():
            return HttpResponseForbidden("Access denied")
        if not path or not os.path.isfile(os.path.join(settings.STATIC_URL, path)):
            return HttpResponseNotFound("Not found")
        mimetypes.init()
        try:
            if "." in path:
                mimetype = mimetypes.types_map[".%s" % path.split(".")[-1]]
            else:
                raise KeyError
        except KeyError:
            mimetype = "text/plain"
        response = HttpResponse()
        response['X-Accel-Redirect'] = "/static/%s" % path
        response['Content-Type'] = mimetype
        return response
And location in nginx:
location /static/ {
    internal;
    root /www/app/;
}

No comments:

Post a Comment