CVE-2023-31047 - How Multiple File Uploads Bypassed Validation in Django (with Exploit Details & Fixes)

Django is one of the most popular web frameworks for building web applications in Python. Over the years, it has proved itself to be secure and reliable. However, even such mature frameworks can have bugs. CVE-2023-31047 is a recent vulnerability which affected Django’s file upload validation between versions 3.2 to 4.2. This long-read post explains the root cause, provides code snippets, demonstrates an exploit, links to original references, and offers advice on mitigation.

Django 4.2 before 4.2.1

Problem:  
It was possible to bypass file validation when using a single form field to upload multiple files. Although Django’s forms.FileField and forms.ImageField never officially supported multiple file uploads, documentation wrongly suggested it could be done. In reality, only the _last file_ of multiple uploaded files was validated—meaning malicious files could slip through.

Django forms are typically defined like this

from django import forms

class UploadForm(forms.Form):
    file = forms.FileField()

When the form is used for a _single_ file upload, all’s well. Now, due to the way HTTP file inputs work, you can upload multiple files with an input like this:

<form method="post" enctype="multipart/form-data">
  <input type="file" name="file" multiple>
  <input type="submit">
</form>

If a user selects several files, Django should receive all of them. But here’s the snag:

Exploit Scenario

Suppose your form is supposed to only accept image files. An attacker submits both evil.php and cute_cat.jpg:

Demonstrating the Flaw (Code Example)

Here’s a small Django snippet to replicate the vulnerable behavior.

forms.py

from django import forms

class FileUploadForm(forms.Form):
    file = forms.FileField()

views.py

from django.shortcuts import render
from .forms import FileUploadForm

def upload_view(request):
    if request.method == "POST":
        form = FileUploadForm(request.POST, request.FILES)
        if form.is_valid():
            # This would save only the last file uploaded!
            handle_uploaded_file(request.FILES['file'])
    else:
        form = FileUploadForm()
    return render(request, 'upload.html', {'form': form})

def handle_uploaded_file(f):
    with open(f.name, 'wb+') as destination:
        for chunk in f.chunks():
            destination.write(chunk)

template/upload.html

<form method="post" enctype="multipart/form-data">
  {% csrf_token %}
  {{ form }}
  <input type="submit" value="Upload">
</form>

Try submitting multiple files: only the last one gets validated, the rest get unchecked.

Upload malicious files (malware, server-side scripts, etc.) as earlier files.

3. If the backend saves all received files (sometimes developers iterate over request.FILES.getlist('file')), the malicious files could slip through undetected.

Official References

- CVE Details - CVE-2023-31047
- Django Security Advisory (2023)
- Django Release Notes for 4.2.1
- Original Django Docs: Uploading multiple files

Suppose the vulnerable code saves all uploaded files

def upload_view(request):
    if request.method == "POST":
        form = FileUploadForm(request.POST, request.FILES)
        if form.is_valid():
            # Insecure: process all uploaded files!
            for f in request.FILES.getlist('file'):
                handle_uploaded_file(f)

Add two files: malware.php (first) and cat.jpg (second).

3. Both are submitted under the same field (file), but only cat.jpg is validated. malware.php passes through.

You can upgrade with pip

pip install --upgrade "Django>=4.1.9"

Validate each file manually.

class MultiFileForm(forms.Form):
    files = forms.FileField(widget=forms.ClearableFileInput(attrs={'multiple': True}))

    def clean_files(self):
        files = self.files.getlist('files')
        for f in files:
            # Add custom validation here
            if not f.content_type in ['image/jpeg', 'image/png']:
                raise forms.ValidationError("Only JPEG and PNG files allowed")
        return files

Conclusion

CVE-2023-31047 was a subtle but important security oversight. It serves as a reminder: always understand your frameworks’ behavior, especially around file uploads. Validate every input, and keep dependencies updated. Stay alert—your users depend on you.

Further Reading

- Django File Upload Docs
- OWASP File Upload Cheat Sheet
- Python Security Best Practices


Stay secure!  
*Written exclusively by ChatGPT for your web security needs.*

Timeline

Published on: 05/07/2023 02:15:00 UTC
Last modified on: 05/16/2023 03:15:00 UTC