Score:1

Apache: restrict serving images to authenticated users

cn flag
Bob

I'm trying to figure out a way to restrict access to a media folder in my apache config. The folder takes uploads from from a Django site and image/pdf uploads are displayed in the site to authenticated users. The problem is, that any unauthenticated schmo can navigate to mysite.com/media/images/pic1.jpg. This shouldn't be possible; I've tried a few things to restrict this behavior, but I think I need a pointer or two.

first try : XSendfile

Xsendfile seemed to work, but it (as the name suggests) sends the file for download, then my page that's supposed to display images doesn't load. So it seems this isn't what I need for my usecase.

second try : rewrite rule

I added some rewrite rules to the apache config:

RewriteCond "%{HTTP_REFERER}" "!^$"
RewriteCond "%{HTTP_REFERER}" "!mysite.com/priv/" [NC]
RewriteRule "\.(gif|jpg|png|pdf)$"    "-"   [F,NC]

All the parts of the site that requires authentication are behind the /priv/ path, so my idea was that if this works then navigating to /media/images/pic1.jpg would be rewriten. But this didn't work either mysite.com/media/images/pic1.jpg still shows the image.

third try : environment

I tried something similar with an environment inside the virtualhost:

<VirtualHost *:80>
    ...
    SetEnvIf Referer "mysite\.com\/priv" localreferer
    SetEnvIf Referer ^$ localreferer
    <FilesMatch "\.(jpg|png|gif|pdf)$">
        Require env localreferer
    </FilesMatch>
    ...
</VirtualHost>

But this also didn't work; I can still navigate directly to the image.

fourth try : Require valid-user

I added Require valid-user to the v-host, but I can't figure out how to check it against the Django user model. This after this change, I would get a prompt to log in each time I loaded a page which displays images (but w/out htaccess etc, there's nothing to auth against and no images are displayed on the site.

I then tried to implement what is described here (https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/apache-auth/), but my django project doesn't like WSGIHandler (as opposed to the default get_wsgi_application()). I get a raise AppRegistryNotReady("Apps aren't loaded yet.") error. It seems like this might be the most reasonable approach, but I don't know how to get the WSGIHandler working, or the approach working with the get_wsgi_application().

I'm aware that I could give the files a hard-to-guess uuid-like name, but this seems like a half-assed solution. So, what's my best strategy to restrict access to the media folder so that these images are only linked within the part of the site where users are authenticated?

Ubuntu 20.04, Apache 2.4

| Edit, following some advice |

auth.py

def check_password(environ, username, password):
    print("---->>>---->>>---->>>---->>>---->>> check_password() has been called  <<<----<<<----<<<----<<<----<<<----")

    return True

#from django.contrib.auth.handlers.modwsgi import check_password

Apache logs show that this script is loaded, but the function apparently isn't executed as the print statement doesn't turn up in the logs. I put a stray print statement in this file and in the wsgi.py file to make sure this strategy makes it to the logs, only that which was in the wsgi.py file made it to the log.

vhost:

<VirtualHost *:80>
    ServerName mysite.com
    ServerAlias mysite.com
    DocumentRoot /path/to/docroot/
    
    Alias /static/ /path/to/docroot/static/

    # Not sure if I need this
    Alias /media/ /path/to/docroot/media/

    <Directory /path/to/docroot/static/>
        Require all granted
    </Directory>

    <Directory /path/to/docroot/media/>
        Require all granted
    </Directory>

    # this is my restricted access directory
    <Directory /path/to/docroot/media/priv/>
        AuthType Basic
        AuthName "Top Secret"
        AuthBasicProvider wsgi
        WSGIAuthUserScript /path/to/docroot/mysite/auth.py
        Require valid-user
    </Directory>

    <Directory /path/to/docroot/mysite/>
        <Files "wsgi.py">
            Require all granted
        </Files>
    </Directory>

    WSGIDaemonProcess open-ancestry-web python-home=/path/to/ENV/ python-path=/path/to/docroot/ processes=10 threads=10
    WSGIProcessGroup mysite-pgroup
    WSGIScriptAlias / /path/to/docroot/mysite/wsgi.py

    LogLevel trace8
    ErrorLog "|/bin/rotatelogs -l /path/to/logs/%Y%m%d-%H%M%S_443errors.log 30"
    CustomLog "|/bin/rotatelogs -l /path/to/logs/%Y%m%d-%H%M%S_443access.log 30" combined
</VirtualHost>

|another edit |

I accepted the answer because everything is now functional. There were lot's of moving parts, which caused the initial problem with the answer. (1) The test check_password function wasn't showing up in the apache logs...well it was turning up at /var/log/apache2/error.log instead of the custom logs that were set up. Not sure why, but ok...

(2) My venv wasn't activated properly and I didn't actually notice this because django is installed on the system Python as well. I copied the activate_this.py script from a virtualenv and added it to my venv and added sth like this to my wsgi file

activate_this = '/path/to/ENV/bin/activate_this.py'
with open(activate_this) as f:
    exec(f.read(), {'__file__': activate_this})

With those things fixed, the check_password function works when called from the wsgi.py file. "works" here means that it restricts access to the folder that unauthed users shouldn't have access to. Users still need to provide credentials twice – once in the regular django view, and once in the browser prompt. This is irritating, but actually my question was about restricting access, so I'll leave it for another day.

The answer's suggestion to call check_password from auth.py is not cooperating with my project. I get errors that suggest it's called before wsgi.py – it seems like the venv is not loaded or the settings are not loaded at the time check_password is called.

Score:1
ve flag

this is what get_wsgi_application is doing:

def get_wsgi_application():
    django.setup(set_prefix=False)     # this will lead to "apps_ready=true"
    return WSGIHandler()

it is setting up the django environment before returning the handler.

The following should do the trick in your wsgi.py:

application = get_wsgi_application()
from django.contrib.auth.handlers.modwsgi import check_password
# the sequence is important!!

In fact the problem is the first line in modwsgi.py:

UserModel = auth.get_user_model()

because get_user_model() will check for apps_ready and all that is done in the moment that python executes the file import!

the better way would be to create a seperate auth.py and first check if it is really called by Apache with a simple print that will go to Apache's error.log:

def check_password(environ, username, password):
    print("***********   check_password() has been called  ********")
    return True

Once this is running you can replace it by the import statement and use djangos check_password().

from django.contrib.auth.handlers.modwsgi import check_password

Then something like the following in httpd-vhosts.conf:

<VirtualHost *:80>

   ....

   <Directory path_to_server_root/secret>
        AuthType Basic
        AuthName "Top Secret"
        AuthBasicProvider wsgi
        WSGIAuthUserScript path_to_wsgi/wsgi.py
        Require valid-user
   </Directory>

</VirtualHost>
cn flag
Bob
Thanks! Two strange things are going on after implementing the changes you suggest. 1. I get a prompt from the browser to authenticate (only the first time, but I imagine it should be handled by my login view). 2. No images are displayed from the secret folder – site loads but the img request returns a 500 error ("auth phase 'check user' gave status 500" in the log files.) I don't know what it means & google isn't being helpful.
Razenstein avatar
ve flag
I change the answer above: create a seperate file "auth.py" (which is anyway the right thing to do and not mix it with the standard wsgi.py - I just wanted to keep it simple in the first answer) and put a test function in - then you can see if Apache is calling your "check_password" function.
Razenstein avatar
ve flag
if you want to display e.g. an image inside a page just use XSendfile as you mention as solution (1) - it works. In the function/view that serves the XSendfile url you can restrict the access just as you would restricted the access to any other view. And by activating XSendFile in Apache any access to the respective directory will be routed to your django app.
cn flag
Bob
And when apache does not call the function in auth.py, what then? Do I need to import it somehow? Or reference it in the apache config somehow? Error logs are the same as the first reply.
Razenstein avatar
ve flag
check if the "WSGIAuthUserScript path_to_auth_script/auth.py" directive is correct. Post your httpd-vhost.conf and auth.py .
cn flag
Bob
posted in the edit
cn flag
Bob
As of now, the private folder is hidden from unauthenticated users, but the user must authenticate via the prompt from the browser after already logging in to the site via regular django auth views. Apache is not calling my auth.py script, so I assume getting that to work will solve the additional login.
mangohost

Post an answer

Most people don’t grasp that asking a lot of questions unlocks learning and improves interpersonal bonding. In Alison’s studies, for example, though people could accurately recall how many questions had been asked in their conversations, they didn’t intuit the link between questions and liking. Across four studies, in which participants were engaged in conversations themselves or read transcripts of others’ conversations, people tended not to realize that question asking would influence—or had influenced—the level of amity between the conversationalists.