Server-side file extension validation in Django 2.1

In another “note to self” installation in this series, I couldn’t quite find the answer to the question: “how do you validate file extensions in a form upload with Django?”. There’s some other ways with ModelForms, but I wasn’t using those.

I know there’s alternate methods

Really, you and I both know that, especially from a security perspective, it’s pretty laughable to only check the file extension. Some other ways we could be accomplishing this:

  • Use the HTML5 attribtue accept attribute, which is easy to bypass, but if it’s an honest user’s mistake, it’ll stop them right there
  • Checking MIME type with magic, which will do a much better job at detecting what exactly is going on inside that file, for the more security-conscious amongst us

I didn’t opt for either of these because reasons.


Extending validate() on your field

Just to quickly cover the basics, if you’re uploading a file in a Django app, you’re probably doing that through a form. And if you’re using a form, you’re probabably using fields. Specifically in the case of the file upload scenario, a forms.FileField. Subsequently, in your views.py you’re (hopefully) calling the form.is_valid() function before doing any processing. That’s where we’ll come in.

If none of the above made sense to you, you should really be reading up on forms in Django.

If it did make sense, you’ll know that when you call is_valid(), Django first cleans, then validates the form, before passing it onto you. Think of proper type casting, dealing with tokens etc. We’re going to subclass the FileField class and add our own validation routine to it.

Subclassing the FileField class

You can do this wherever you like, though it probably makes most sense to do this in forms.py if this is the extent of your edits. If you’re going further in that, you may want to re-org. Subclassing is standard Python stuff and as simple as this:

from django import forms
class CsvFileField(forms.FileField):
    def validate(self, value):
        pass

Extending validate()

Remember, by the time our custom validation function rolls around, Django will already have applied cleaning and its standard validation routines. That means that the value argument you see below is an UploadedFile, you can read more about those here. Long story short, we can grab its name attribute to check the file extension:

import os
class CsvFileField(forms.FileField):
    def validate(self, value):
        # First run the parent class' validation routine
        super().validate(value)
        # Run our own file extension check
        file_extension = os.path.splitext(value.name)[1]
        if file_extension != '.csv':
            raise ValidationError(
                 ('Invalid file extension'),
                 code='invalid'
            )

Two things to note:

  • We’re first running the parent class’ validation routine
  • For good security practice, instead of this extension check, this would be a good place to run the magic library and do some decent file-extension checking