Restrict image access

I’m trying to set up a simple score sharing server, where i want to display current progress in a class only to authenticated university students stored in an sqlite database within the django framework. I also want to provide an image of the written exam itself. Since i deal with sensitive content like e-mail addresses, names, student id’s and the exam images it is the first time I really need to put a focus on data security.

Hopefully an examplary files structure can clarify my question:

score_project
    |-settings.py
    |-urls.py
    |-etc.
    |-score_app
    |    |-static
    |    |    |-img
    |    |    |    |-exam_user1.jpeg
    |    |    |    |-exam_user2.jpeg
    |    |    |-etc.
    |    |-templates
    |    |    |-score
    |    |         |-index.html
    |    |         |-etc.
    |    |-models.py
    |    |-views.py
    |    |-urls.py
    |-db.sqlite3
    |-manage.py

I restricted all return values from views properly with standard user authentication, but when it comes to the images I was still wondering if there is really no work around…

Now, assuming I did the user authentication properly, when requesting via GET an url like “http://localhost:8000/” i will get a restricted view of index.html, not providing sensitive data to the client. And if authenticated i get displayed the authenticated users exam and score.

But what is hindering me to just input the full path of another students exam like “http:localhost:8000/score_app/static/img/exam_user2.jpeg”?
Deriving from that, what is hindering anyone to access the sqlite database, lying on the webserver?

Is there a django built-in option to limit access to restricted files within my django framework? How would i secure these files on a web hosting server?

I may not have understood some fundamentals of web services, hence I’m struggeling in asking the right questions.

Thanks for any hints about where i can read into that topic, in advance!

See the threads at

for some information and ideas on securing files (both static and media).

In a proper production-quality deployment, your static and media files reside outside your project. Your web server (nginx, apache, etc) should have direct access to those files, but should not (and has no need to) have access to your project iself.

1 Like

Thanks for your quick answer, I’m starting to suspect ChatGPT being a cheap copy of you. But OpenAI will have hard times to reach your level of professionalism. I really appreciate that you take the time to elevate hobby projects like mine.

Anyways, I can imagine that after reaching a certain level of expertiese it’s getting harder and harder to think into the very beginner problems. So I want to give it a try to document my approach to a solution in the hope being able to give something back to the inspirational free-open-source-computer-science-community.

So, after spending hours on that topic I learned about Nginx and Apache.
I learned that Apache is the server utilized by my web hosting service STRATO (closely connected to this topic here). Similar to them it seems i have no access to “mod_wsgi” and it is unlikely (though I have not yet checked) there is a way to add a module to the apache installation like they state as an alternative installation of mod_wsgi in their documentation here.

I came to the conclusion that my hope (restricting certain files) may be the right configuration within a “.htaccess” file, as discussed here.

Do you think I’m on the right track?

Interesting that you say a server should have access to the files, but not the project. I would have expected it to be exactly the other way around, since my django apps access the frameworks SQLite database and their static and media files. What you’re saying means that Apache (in my case) would serve the media and static files to clients, whereas the django project shouldn’t.
And still I would expect Apache to communicate with my Django project somehow?

Since this is the first time i want to make a project kind of production ready, I put a lot into question what seemed fine so far. So all the static file handling i utilized when building my django project is only for development, but i should get rid of it in production?

Again, thanks in advance for any help to stay on track reaching my goal!

Unfortunately, I think the answer here for that is “no”. I don’t see where the .htaccess file provides the necessary degree of flexibility to use something that handles the X-sendfile header. (More on this to come a little further down)

Actually, that’s not quite an accurate interpretation of what I said. Let me try explaining it a different way.

Let’s start with defining three different directories:

  • /project - All your Django code
  • /static - Where the static files are stored
  • /media - Where your user-uploaded files are stored
    (They probably aren’t going to have these precise names, I’m using them as generic representations of real directories in your file system.)

Your web server process (Apache, nginx, etc) does not need direct, file-level access to /project. You’re running a separate python process, that is executing the code. You’re exchanging data between the web server and python processes through a socket (either tcp or domain). But the web server itself has no need to access those files.

Python needs to access all the files in /project to be able to run them.

Your web server needs to be able to directly read the files in both /static and /media, in order to serve them from http requests that have been received.

Your python process also needs to be able to access the /media directory, because it’s your Django application that handles uploaded files and causes them to be saved.

The only item not identified here is whether Python needs access to /static. At some point, something running your python code does need access to /static in order for collectstatic to work.

Now, from our deployment standards, the process that runs Django does not need access to /static, because those files don’t change while the system is operational, and it’s nginx that serves those files to the browser.

However, if:

  • You need to limit access to specific files to specific users
    and
  • You cannot install the correct modules in your web server to manage web server access to files from your application

then you may need to serve your files from within your Django app - and that’s where tools like whitenoise come into play.

Correct. The docs cover the differences between managing static files in development and production.

See:

There’s a paragraph at the Static file development view docs that kind of sums this up:

The static files tools are mostly designed to help with getting static files successfully deployed into production. This usually means a separate, dedicated static file server, which is a lot of overhead to mess with when developing locally. Thus, the staticfiles app ships with a quick and dirty helper view that you can use to serve files locally in development.

Getting static and media files set up correctly in production are among the most “fiddly” and error-prone aspects of a production deployment the first time, mostly because there aren’t any “cookbook” solutions you can rely upon because of all the different factors involved.

Oh dear, reading your previous response again makes now more and more sense. I didn’t follow the “whitenoise” option up in the first place because the topics you linked seemed specific to Nginx server.

Just to clarify that one, let me rephrase: Separating static files to be served by Nginx/Apache is more a best practice (in terms of performance and maybe vulnerabilities), rather than a real technical requirement? And there is indeed middleware available for the django framework (like “whitenoise”, and now i suspect “django-downloadview” and “django-protected-media” to be to consider too) to serve exactly that purpose while maintaining good security (like user authentication)?

However, you saved hours, maybe days, for me potentially configuring a “.htaccess” file to an extend where i finally realize it’s not possible to do it that way.

Only one more question came up, since my web hosting service provides CGI support in python language. I have not used CGI yet but it seemes in general to allow exactly what i want to achieve, but I’ve read it would start a django process on every request and bring a huge security risk with its use (discussed here). Would you consider CGI another possible attempt if the “whitenoise” doesn’t work?

Correct. This is why I refer to this as a “production-quality deployment”. (There are situations where “runserver” may well be the right choice - and I happen to do a fair amount of work in that space. But they’re not what I would call a true deployment, even though those sites are in production and being used.)

Yes to “whitenoise”. I know almost nothing about the other two (“downloadview” and “protected-media”). It appears that they may be designed to work both with a webserver using the proper header, or to serve the files directly. (My comment from 2021 about never having used either one is still accurate.)

Short answer: CGI with Django? No.

Long answer: See the thread at FastCGI + Django
and the various resources linked within it, primarily #2407 (CGI Support for django) – Django.

Thanks a lot for your help! At the very beginning it is unvaluable for me to get a second oppinion at some points.

I implemented all the features it should have. User authentication, SQLite database containing several models to represent students performance in exams, a javascript grading tool and the views are restricting to authenticated users.

But I’m trying for hours now to get “whitenoise” in django running. but i failed constantly to test in the deployment mode.

As long as i set DEBUG=True,
everything works just fine.

index.html DEBUG=True

as discibed here i run

python ./manage.py collectstatic

set DEBUG=False

python ./manage.py runserver

and it failes do load the images. Notably the CSS and JavaScript seems not to be a problem:

index.html DEBUG=False

The console prints the following errors:

Performing system checks...

System check identified some issues:

WARNINGS:
?: (staticfiles.W004) The directory 'C:\Users\Fungos\che119_score\che119_score\static' in the STATICFILES_DIRS setting does not exist.

System check identified 1 issue (0 silenced).                                                 :38:21] "GET /static/score/styles/styles.css HTTP/1.1" 200 4792
May 19, 2024 - 19:30:38                                                        [19/May/2024 18
Django version 5.0.6, using settings 'che119_score.settings'
Starting development server at http://127.0.0.1:8000/                          79
Quit the server with CTRL-BREAK.                                               79

[19/May/2024 19:30:39] "GET / HTTP/1.1" 200 9337
[19/May/2024 19:30:39] "GET /static/score/styles/styles.css HTTP/1.1" 304 0
[19/May/2024 19:30:39] "GET /static/score/scripts/grading.js HTTP/1.1" 304 0
[19/May/2024 19:30:39] "GET /static/score/img/test6/1234_1.jpeg HTTP/1.1" 404 179
[19/May/2024 19:30:39] "GET /static/score/img/test6/1234_2.jpeg HTTP/1.1" 404 179

There might be an issue with the STATICFILES_DIRS settings, but i tried like anything, but without any success. Even when I force it to point at the location of the static folder it shows the exact same warning.

che119_score/settings.py

BASE_DIR = Path(__file__).resolve().parent.parent

DEBUG = True

ALLOWED_HOSTS = ["127.0.0.1", "localhost", '*']

INSTALLED_APPS = [
    "whitenoise.runserver_nostatic", # suppresses djangos built in static file handling.
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    "score",
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'whitenoise.middleware.WhiteNoiseMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]


STATICFILES_DIRS = [
    BASE_DIR / 'static',
]
STATIC_URL = 'static/'

STATIC_ROOT = BASE_DIR / 'staticfiles'

File Structure

che119_score
|    |----che119_score
|    |    |----__init__.py
|    |    |----asgi.py
|    |    |----settings.py
|    |    |----urls.py
|    |    |----wsgi.py
|    |
|    |----score
|    |    |----migrations (...database specific...)
|    |    |----static
|    |         |----score
|    |         |    |----img
|    |         |    |    |----Test1
|    |         |    |    |    |----1234_1.jpeg
|    |         |    |    |    |----1234_2.jpeg
|    |         |    |    |  
|    |         |    |    |----Test2
|    |         |    |    |    |----1234_1.jpeg
|    |         |    |    |    |----1234_2.jpeg
|    |         |    |    |  
|    |         |    |    |----Test3
|    |         |    |    |    |----1234_1.jpeg
|    |         |    |    |    |----1234_2.jpeg
|    |         |    |    |  
|    |         |    |    |----Test4
|    |         |    |    |    |----1234_1.jpeg
|    |         |    |    |    |----1234_2.jpeg
|    |         |    |    |  
|    |         |    |    |----Test5
|    |         |    |    |    |----1234_1.jpeg
|    |         |    |    |    |----1234_2.jpeg
|    |         |    |    |  
|    |         |    |    |----Test6
|    |         |    |    |    |----1234_1.jpeg
|    |         |    |    |    |----1234_2.jpeg
|    |         |    |    |  
|    |         |    |    |----Test7
|    |         |    |    |    |----1234_1.jpeg
|    |         |    |    |    |----1234_2.jpeg
|    |         |    |    |  
|    |         |    |
|    |         |    |
|    |         |    |
|    |         |    |----scripts
|    |         |    |    |----grading.js
|    |         |    |
|    |         |    |----styles
|    |         |         |----styles.css
|    |         |    
|    |         |----templates
|    |         |    |----score
|    |         |         |----detail.html
|    |         |         |----index.html
|    |         |         |----layout.html
|    |         |         |----login.html
|    |         |
|    |         | ----__init__.py
|    |         | ----admin.py
|    |         | ----apps.py
|    |         | ----models.py
|    |         | ----tests.py
|    |         | ----urls.py
|    |         | ----views.py
|    |
|    |----staticfiles
|    |    |----admin
|    |    |    |----css
|    |    |    |----img
|    |    |    |----js
|    |    |   
|    |    |----score
|    |         |----img
|    |         |    |----Test1
|    |         |    |    |----1234_1.jpeg
|    |         |    |    |----1234_2.jpeg
|    |         |    |  
|    |         |    |----Test2
|    |         |    |    |----1234_1.jpeg
|    |         |    |    |----1234_2.jpeg
|    |         |    |  
|    |         |    |----Test3
|    |         |    |    |----1234_1.jpeg
|    |         |    |    |----1234_2.jpeg
|    |         |    |  
|    |         |    |----Test4
|    |         |    |    |----1234_1.jpeg
|    |         |    |    |----1234_2.jpeg
|    |         |    |  
|    |         |    |----Test5
|    |         |    |    |----1234_1.jpeg
|    |         |    |    |----1234_2.jpeg
|    |         |    |  
|    |         |    |----Test6
|    |         |    |    |----1234_1.jpeg
|    |         |    |    |----1234_2.jpeg
|    |         |    |  
|    |         |    |----Test7
|    |         |    |    |----1234_1.jpeg
|    |         |    |    |----1234_2.jpeg
|    |         |    |  
|    |         |
|    |         |
|    |         |
|    |         |----scripts
|    |         |    |----grading.js
|    |         |
|    |         |----styles
|    |              |----styles.css
|    |
|    |----db.sqlite3
|    |----manage.py
|
|----venv

che119_score/urls.py

from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path('admin/', admin.site.urls),
    path("", include("score.urls")),
]

score/urls.py

urlpatterns = [
    path('login/', views.log_in, name='log_in'),
    path("logout/", views.log_out, name="log_out"),
    path("detail/<int:exam_number>", views.detail, name="detail"),
    path("", views.score, name="score"),
]

I took care to refere the images via the static tag, instead of harcoding urls:

index.html

{% extends 'score/layout.html' %}
{% load static %}
{% block body %}
<body>
    

    <div class="scoreParticipation">
        <h2>Mitarbeitspunkte</h2>
        <h3>{{ participation.reached_points }} von {{ participation.max_points }} Punkten</h3>

    </div>

    {% for exam in exams %}
    <div class="scoreTables">
    <h2>Test {{ exam.exam_number }}</h2>
    <h3>{{ exam.reached_points }} von {{ exam.max_points }} Punkten</h3>
    <label>Thema: {{ exam.exam_topic }}</label>
        <br>
        <br>
        <div>
            {% block images %}
            <div>
                {% for exam_number in exam_numbers %}
                    {% if exam.exam_number|slugify == exam_number %}
                        {% with 'score/img/test'|add:exam_number|add:'/'|add:immatriculation_number|add:'_1.jpeg' as image_static %}
                            <a href="{% url 'detail' exam.exam_number %}"><img class="pages" style="width: 100px; height: 150px;" alt="Vorderseite Test {{ exam.exam_number }} von {{student.student_user.first_name}} {{student.student_user.last_name}}" src="{% static image_static %}"></a>                    
                            {% endwith %}
                    {% endif %}
                {% endfor %}                
                {% for exam_number in exam_numbers %}
                    {% if exam.exam_number|slugify == exam_number %}
                        {% with 'score/img/test'|add:exam_number|add:'/'|add:immatriculation_number|add:'_2.jpeg' as image_static %}
                            <a href="{% url 'detail' exam.exam_number %}"><img class="pages" style="width: 100px; height: 150px;" alt="Rückseite Test {{ exam.exam_number }} von {{student.student_user.first_name}} {{student.student_user.last_name}}" src="{% static image_static %}"></a>
                        {% endwith %}
                    {% endif %}
                {% endfor %}
            </div>
            {% endblock %}
        </div> 
    </div>
    {% endfor %}
    
</body>
{% endblock %}

views.py

... 

def score(request):
    if request.method == "GET":
        if request.user.is_authenticated:
            student = list(Student.objects.filter(student_user=request.user))[0]
            exams = WrittenExam.objects.filter(student=student)
            participation = list(Participation.objects.filter(student=student))[0]
            exam_numbers = [str(exam.exam_number) for exam in exams]
            context = {
                "range": range(1,9),
                "student": student,
                "exams": exams,
                "exam_numbers": exam_numbers,
                "immatriculation_number": str(student.immatriculation_number),
                "participation": participation,
                "title": "CHE119 Beurteilungen SS2024"
            }
            return render(request, "score/index.html", context=context)
        else:
            context = {
                "title": "Anmeldung Punktestand CHE119",
                "range": range(1,9),
            }
            return render(request, "score/login.html", context=context)
    else:
        return Http404("Not available")

...

May not be the cleanest code, but it turned out to be by far more time consuming then expected to get this into deployment …

Maybe the problem is that i run the deployment settings on localhost? But the documentation of whitenoise explicitly states that one can assure it is working properly by running the commands i stated at the very top.

Thanks in advance for any comments or hints how i can get Django running with serving static files via whitenoise.

I’m far from being a “WhiteNoise” expert, but I did notice a couple things.

First, this:

is only used in development to override runserver static file handling.

The other is that the logs you’re showing have:

But the directory name you’re showing is:

… which is not the same. (File and directory names are case-sensitive)

1 Like

You made my day, thank you so much! You are absolutely right, i simply had a typo in the path… I changed it immediately and it runs perfectly fine now with either DEBUG is True or False.

But still it is weird that django was able to compensate that in development mode, but it failed in production.

I learned a lot about static file handling, Apache and Django app deployment. I’ll give a final update when i can make it running at the Apache infrastructure, but i guess I will try to host it on “Pythonanywhere” first.

Anyways, with that I guess i can close this thread since the initial question is definitely answered with “whitenoise”.

Is your production server the same Operating System as your development server? If not, what are they?

I may understand the terms a bit different than an experienced developer, but with “development” i mean debug is on, with “production” debug is off ^^

I run them both on my local machine which is Windows 10 Home.

find the finished project in my GitHub or deployed on Pythonanywhere. Did not yet make it running on Apache server.

Note this amazing step-by-step guide to deploy a Django app on pythonanywhere: