From 8c75abf02e6860c3c40d6d4a1161f62a01867cf6 Mon Sep 17 00:00:00 2001 From: jvirgovic Date: Tue, 30 Jun 2026 01:38:27 +0000 Subject: [PATCH] feat(skills): harden the file upload validation section in django-security (#2338) * feat(skills): harden the file upload validation section in django-security * Update skills/django-security/SKILL.md Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * add missing stuff to second code block * add import to the top of the code block --------- Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- skills/django-security/SKILL.md | 68 ++++++++++++++++++++++++++++----- 1 file changed, 59 insertions(+), 9 deletions(-) diff --git a/skills/django-security/SKILL.md b/skills/django-security/SKILL.md index c81a6643..b95b9795 100644 --- a/skills/django-security/SKILL.md +++ b/skills/django-security/SKILL.md @@ -392,27 +392,77 @@ def webhook_view(request): ```python import os +import magic # pip install python-magic from django.core.exceptions import ValidationError -def validate_file_extension(value): - """Validate file extension.""" - ext = os.path.splitext(value.name)[1] - valid_extensions = ['.jpg', '.jpeg', '.png', '.gif', '.pdf'] - if not ext.lower() in valid_extensions: - raise ValidationError('Unsupported file extension.') +ALLOWED_MIMES = { + 'image/jpeg', 'image/png', 'image/gif', 'application/pdf', +} + +MIME_TO_EXTENSIONS = { + 'image/jpeg': {'.jpg', '.jpeg'}, + 'image/png': {'.png'}, + 'image/gif': {'.gif'}, + 'application/pdf': {'.pdf'}, +} + +def validate_file_type(value): + """Validate file type using magic bytes and cross-check extension.""" + mime = magic.from_buffer(value.read(2048), mime=True) + value.seek(0) + + if mime not in ALLOWED_MIMES: + raise ValidationError('Unsupported file type.') + + ext = os.path.splitext(value.name)[1].lower() + if ext not in MIME_TO_EXTENSIONS.get(mime, set()): + raise ValidationError('File extension does not match file content.') def validate_file_size(value): """Validate file size (max 5MB).""" - filesize = value.size - if filesize > 5 * 1024 * 1024: + if value.size > 5 * 1024 * 1024: raise ValidationError('File too large. Max size is 5MB.') # models.py class Document(models.Model): file = models.FileField( upload_to='documents/', - validators=[validate_file_extension, validate_file_size] + validators=[validate_file_type, validate_file_size] ) + +``` + +For environments where installing libmagic is difficult (e.g., minimal containers), +use the pure-Python `filetype` package as an alternative: + +```python +import os +from django.core.exceptions import ValidationError + +import filetype # pip install filetype + +ALLOWED_MIMES = { + 'image/jpeg', 'image/png', 'image/gif', 'application/pdf', +} + +MIME_TO_EXTENSIONS = { + 'image/jpeg': {'.jpg', '.jpeg'}, + 'image/png': {'.png'}, + 'image/gif': {'.gif'}, + 'application/pdf': {'.pdf'}, +} + +def validate_file_type(value): + """Validate file type using magic bytes.""" + kind = filetype.guess(value.read(2048)) + value.seek(0) + + if kind is None or kind.mime not in ALLOWED_MIMES: + raise ValidationError('Unsupported file type.') + + ext = os.path.splitext(value.name)[1].lower() + if ext not in MIME_TO_EXTENSIONS.get(kind.mime, set()): + raise ValidationError('File extension does not match file content.') ``` ### Secure File Storage