✨(backend) add an allowed file mimetype list

Allong the allowed extension list we added an allowed mimetype list. We
combine both to ensure that the user is allowed to make the upload. If
the mimetype is not allowed, then the file and the item are deleted.
This commit is contained in:
Manuel Raynaud
2026-01-09 10:43:56 +01:00
parent ecdd68f43b
commit 3901e57cab
5 changed files with 253 additions and 4 deletions
+1
View File
@@ -45,6 +45,7 @@ This document lists all configurable environment variables for the Drive applica
| `FEATURES_ALPHA` | Enable alpha features | `False` |
| `FEATURES_INDEXED_SEARCH` | Enable the search of indexed files through the API | `True` |
| `FILE_EXTENSIONS_ALLOWED` | List of file extension allowed to be uploaded | See in the settings.py file |
| `FILE_MIMETYPE_ALLOWED` | List of file mimetype allowed to be uploaded | See in the setings.py file |
| `FRONTEND_THEME` | Frontend theme configuration | `None` |
| `FRONTEND_FEEDBACK_BUTTON_SHOW` | Show feedback button | `False` |
| `FRONTEND_FEEDBACK_BUTTON_IDLE` | Make feedback button idle (e.g. to bind to external library) | `False` |
+5 -1
View File
@@ -436,7 +436,11 @@ class CreateItemSerializer(ItemSerializer):
if settings.RESTRICT_UPLOAD_FILE_TYPE:
_root, extension = splitext(attrs["filename"])
if extension not in settings.FILE_EXTENSIONS_ALLOWED:
logger.info("create_item: file extension not allowed %s for filename %s", extension, attrs["filename"])
logger.info(
"create_item: file extension not allowed %s for filename %s",
extension,
attrs["filename"],
)
raise serializers.ValidationError(
{"filename": _("This file extension is not allowed.")},
code="item_create_file_extension_not_allowed",
+22 -3
View File
@@ -682,9 +682,7 @@ class ItemViewSet(
entitlements_backend = get_entitlements_backend()
can_upload = entitlements_backend.can_upload(self.request.user)
if not can_upload["result"]:
item.soft_delete()
item.hard_delete()
process_item_deletion.delay(item.id)
self._complete_item_deletion(item)
raise drf.exceptions.PermissionDenied(
detail=can_upload.get(
"message", "You do not have permission to upload files."
@@ -713,6 +711,21 @@ class ItemViewSet(
# Use improved MIME type detection combining magic bytes and file extension
mimetype = utils.detect_mimetype(file_head, filename=item.filename)
if (
settings.RESTRICT_UPLOAD_FILE_TYPE
and mimetype not in settings.FILE_MIMETYPE_ALLOWED
):
self._complete_item_deletion(item)
logger.info(
"upload_ended: mimetype not allowed %s for filename %s",
mimetype,
item.filename,
)
raise drf.exceptions.ValidationError(
detail="The file type is not allowed.",
code="file_type_not_allowed",
)
item.upload_state = models.ItemUploadStateChoices.ANALYZING
item.mimetype = mimetype
item.size = file_size
@@ -737,6 +750,12 @@ class ItemViewSet(
return drf_response.Response(serializer.data, status=status.HTTP_200_OK)
def _complete_item_deletion(self, item):
"""Completely delete an item."""
item.soft_delete()
item.hard_delete()
process_item_deletion.delay(item.id)
@drf.decorators.action(
detail=False,
methods=["get"],
@@ -1,5 +1,6 @@
"""Test related to item upload ended API."""
import logging
from io import BytesIO
from unittest import mock
@@ -237,3 +238,67 @@ def test_api_item_upload_ended_entitlements_backend_returns_falsy_custom_message
}
assert not models.Item.objects.filter(id=item.id).exists()
def test_api_item_upload_ended_mimetype_not_allowed(settings, caplog):
"""
Test that the API returns a 400 when the mimetype is not allowed.
Item should be deleted and the file should be deleted from the storage.
"""
settings.RESTRICT_UPLOAD_FILE_TYPE = True
settings.FILE_MIMETYPE_ALLOWED = ["application/pdf"]
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
item = factories.ItemFactory(type=ItemTypeChoices.FILE, filename="my_file.txt")
factories.UserItemAccessFactory(item=item, user=user, role="owner")
default_storage.save(
item.file_key,
BytesIO(b"my prose"),
)
with caplog.at_level(logging.INFO):
response = client.post(f"/api/v1.0/items/{item.id!s}/upload-ended/")
assert response.status_code == 400
assert (
"upload_ended: mimetype not allowed text/plain for filename my_file.txt"
in caplog.text
)
assert not models.Item.objects.filter(id=item.id).exists()
assert not default_storage.exists(item.file_key)
def test_api_item_upload_ended_mimetype_not_allowed_not_checking_mimetype(settings):
"""
Test that the API returns a 200 when the mimetype is not allowed but not checking the mimetype.
"""
settings.RESTRICT_UPLOAD_FILE_TYPE = False
settings.FILE_MIMETYPE_ALLOWED = ["application/pdf"]
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
item = factories.ItemFactory(type=ItemTypeChoices.FILE, filename="my_file.txt")
factories.UserItemAccessFactory(item=item, user=user, role="owner")
default_storage.save(
item.file_key,
BytesIO(b"my prose"),
)
with mock.patch.object(malware_detection, "analyse_file") as mock_analyse_file:
response = client.post(f"/api/v1.0/items/{item.id!s}/upload-ended/")
mock_analyse_file.assert_called_once_with(item.file_key, item_id=item.id)
assert response.status_code == 200
item.refresh_from_db()
assert item.upload_state == ItemUploadStateChoices.ANALYZING
assert item.mimetype == "text/plain"
assert item.size == 8
assert response.json()["mimetype"] == "text/plain"
+160
View File
@@ -552,6 +552,166 @@ class Base(Configuration):
environ_prefix=None,
)
FILE_MIMETYPE_ALLOWED = values.ListValue(
[
"application/epub+zip",
"application/gml+xml",
"application/gpx+xml",
"application/gzip",
"application/json",
"application/msword",
"application/octet-stream",
"application/pdf",
"application/pgp-signature",
"application/pkcs10",
"application/pkcs7-signature",
"application/pkix-cert",
"application/postscript",
"application/rtf",
"application/sql",
"application/vnd.android.package-archive",
"application/vnd.businessobjects",
"application/vnd.chemdraw+xml",
"application/vnd.dna",
"application/vnd.dynageo",
"application/vnd.google-earth.kml+xml",
"application/vnd.google-earth.kmz",
"application/vnd.koan",
"application/vnd.mcd",
"application/vnd.ms-excel",
"application/vnd.ms-excel.sheet.binary.macroenabled.12",
"application/vnd.ms-excel.sheet.macroenabled.12",
"application/vnd.ms-excel.template.macroenabled.12",
"application/vnd.ms-fontobject",
"application/vnd.ms-outlook",
"application/vnd.ms-pki.stl",
"application/vnd.ms-powerpoint",
"application/vnd.ms-powerpoint.presentation.macroenabled.12",
"application/vnd.ms-powerpoint.slideshow.macroenabled.12",
"application/vnd.ms-powerpoint.template.macroenabled.12",
"application/vnd.ms-project",
"application/vnd.ms-word.document.macroenabled.12",
"application/vnd.ms-word.template.macroenabled.12",
"application/vnd.ms-xpsdocument",
"application/vnd.oasis.opendocument.chart",
"application/vnd.oasis.opendocument.chart-template",
"application/vnd.oasis.opendocument.database",
"application/vnd.oasis.opendocument.formula",
"application/vnd.oasis.opendocument.graphics",
"application/vnd.oasis.opendocument.graphics-template",
"application/vnd.oasis.opendocument.image",
"application/vnd.oasis.opendocument.image-template",
"application/vnd.oasis.opendocument.presentation",
"application/vnd.oasis.opendocument.presentation-template",
"application/vnd.oasis.opendocument.spreadsheet",
"application/vnd.oasis.opendocument.spreadsheet-template",
"application/vnd.oasis.opendocument.text",
"application/vnd.oasis.opendocument.text-master",
"application/vnd.oasis.opendocument.text-template",
"application/vnd.oasis.opendocument.text-web",
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
"application/vnd.openxmlformats-officedocument.presentationml.slideshow",
"application/vnd.openxmlformats-officedocument.presentationml.template",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"application/vnd.openxmlformats-officedocument.spreadsheetml.template",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/vnd.openxmlformats-officedocument.wordprocessingml.template",
"application/vnd.palm",
"application/vnd.realvnc.bed",
"application/vnd.sun.xml.calc",
"application/vnd.sun.xml.calc.template",
"application/vnd.sun.xml.draw",
"application/vnd.sun.xml.draw.template",
"application/vnd.sun.xml.impress",
"application/vnd.sun.xml.math",
"application/vnd.sun.xml.writer",
"application/vnd.sun.xml.writer.global",
"application/vnd.sun.xml.writer.template",
"application/vnd.visio",
"application/vnd.yamaha.openscoreformat",
"application/x-7z-compressed",
"application/x-bzip",
"application/x-bzip2",
"application/x-debian-package",
"application/x-empty",
"application/x-freearc",
"application/x-keepass",
"application/x-keepass2",
"application/x-msaccess",
"application/x-msmetafile",
"application/x-mspublisher",
"application/x-python-bytecodeapplication/x-shellscript",
"application/x-python-code",
"application/x-rar",
"application/x-rar-compressed",
"application/x-research-info-systems",
"application/x-sh",
"application/x-sql",
"application/x-subrip",
"application/x-tar",
"application/x-tex",
"application/x-tgif",
"application/x-x509-ca-cert",
"application/x-xz",
"application/xml",
"application/yaml",
"application/zip",
"audio/aac",
"audio/basic",
"audio/mp4",
"audio/mpeg",
"audio/wav",
"audio/webm",
"audio/x-aac",
"audio/x-aiff",
"audio/x-wav",
"chemical/x-cif",
"font/collection",
"font/otf",
"font/ttf",
"font/woff",
"font/woff2",
"image/bmp",
"image/gif",
"image/heic",
"image/jpeg",
"image/png",
"image/svg+xml",
"image/tiff",
"image/vnd.adobe.photoshop",
"image/vnd.djvu",
"image/vnd.dwg",
"image/vnd.dxf",
"image/webp",
"image/x-icon",
"message/rfc822",
"model/iges",
"text/calendar",
"text/csv",
"text/markdown",
"text/plain",
"text/tab-separated-values",
"text/vtt",
"text/x-c",
"text/x-python",
"text/x-vcard",
"video/3gpp",
"video/3gpp2",
"video/mp2t",
"video/mp4",
"video/mpeg",
"video/quicktime",
"video/webm",
"video/x-m4v",
"video/x-matroska",
"video/x-msvideo",
# Fallback
"application/octet-stream",
],
environ_name="FILE_MIMETYPE_ALLOWED",
environt_prefix=None,
)
ITEM_PREVIEWABLE_MIME_TYPES = values.ListValue(
[
"image/",