diff --git a/avatar/conf.py b/avatar/conf.py index 4a84407..1a66e21 100644 --- a/avatar/conf.py +++ b/avatar/conf.py @@ -22,6 +22,7 @@ class AvatarConf(AppConf): HASH_USERDIRNAMES = False EXPOSE_USERNAMES = False ALLOWED_FILE_EXTS = None + ALLOWED_MIMETYPES = None CACHE_TIMEOUT = 60 * 60 STORAGE = settings.DEFAULT_FILE_STORAGE CLEANUP_DELETED = True diff --git a/avatar/forms.py b/avatar/forms.py index 0cceb42..745bae3 100644 --- a/avatar/forms.py +++ b/avatar/forms.py @@ -29,6 +29,34 @@ class UploadAvatarForm(forms.Form): def clean_avatar(self): data = self.cleaned_data["avatar"] + if settings.AVATAR_ALLOWED_MIMETYPES: + try: + import magic + except ImportError: + raise ImportError( + "python-magic library must be installed in order to use uploaded file content limitation" + ) + + # Construct 256 bytes needed for mime validation + magic_buffer = bytes() + for chunk in data.chunks(): + magic_buffer += chunk + if len(magic_buffer) >= 256: + break + + # https://github.com/ahupp/python-magic#usage + mime = magic.from_buffer(magic_buffer, mime=True) + if mime not in settings.AVATAR_ALLOWED_MIMETYPES: + raise forms.ValidationError( + _( + "File content is invalid. Detected: %(mimetype)s Allowed content types are: %(valid_mime_list)s" + ) + % { + "valid_mime_list": ", ".join(settings.AVATAR_ALLOWED_MIMETYPES), + "mimetype": mime, + } + ) + if settings.AVATAR_ALLOWED_FILE_EXTS: root, ext = os.path.splitext(data.name.lower()) if ext not in settings.AVATAR_ALLOWED_FILE_EXTS: diff --git a/tests/data/test.tiff b/tests/data/test.tiff new file mode 100644 index 0000000..c3d899b Binary files /dev/null and b/tests/data/test.tiff differ diff --git a/tests/requirements.txt b/tests/requirements.txt index cee13f4..04d12f0 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,2 +1,3 @@ coverage==6.2 django +python-magic diff --git a/tests/tests.py b/tests/tests.py index cce39ac..3045a08 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -117,15 +117,27 @@ class AvatarTests(TestCase): self.assertEqual(avatar.user, self.user) self.assertTrue(avatar.primary) + # We allow the .tiff file extension but not the mime type + @override_settings(AVATAR_ALLOWED_FILE_EXTS=(".png", ".gif", ".jpg", ".tiff")) + @override_settings( + AVATAR_ALLOWED_MIMETYPES=("image/png", "image/gif", "image/jpeg") + ) + def test_unsupported_image_format_upload(self): + """Check with python-magic that we detect corrupted / unapprovd image files correctly""" + response = upload_helper(self, "test.tiff") + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.redirect_chain), 0) # Redirect only if it worked + self.assertNotEqual(response.context["upload_avatar_form"].errors, {}) + + @override_settings(AVATAR_ALLOWED_FILE_EXTS=(".jpg", ".png")) def test_image_without_wrong_extension(self): - # use with AVATAR_ALLOWED_FILE_EXTS = ('.jpg', '.png') response = upload_helper(self, "imagefilewithoutext") self.assertEqual(response.status_code, 200) self.assertEqual(len(response.redirect_chain), 0) # Redirect only if it worked self.assertNotEqual(response.context["upload_avatar_form"].errors, {}) + @override_settings(AVATAR_ALLOWED_FILE_EXTS=(".jpg", ".png")) def test_image_with_wrong_extension(self): - # use with AVATAR_ALLOWED_FILE_EXTS = ('.jpg', '.png') response = upload_helper(self, "imagefilewithwrongext.ogg") self.assertEqual(response.status_code, 200) self.assertEqual(len(response.redirect_chain), 0) # Redirect only if it worked