mirror of
https://github.com/suitenumerique/drive.git
synced 2026-09-27 20:15:04 +02:00
✨(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:
@@ -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` |
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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/",
|
||||
|
||||
Reference in New Issue
Block a user