From 244e1a0c7d7e521e3bcce4995952f8a988dd7b55 Mon Sep 17 00:00:00 2001 From: Vinit Kumar Date: Mon, 25 Sep 2023 00:47:49 +0530 Subject: [PATCH] fix: decompression bomb attack in the Filer Filer allowed for the images for very high pixels (height * width) to be uploaded. This would cause crash and failures when the high pixels exceeded what is allowed by Pillow Image MAX_IMAGE_PIXELS value. This is an issue because even though the image is possible to be created and attached to the page, it would never work as PIL always fails to thumbnails such high pixel image and crashes causing crash and high memory usages in such pages. This patch, fixes this issues in the bud as it wouldn't allow such files to be uploaded via FILER itself. It also allows to set a lower limit FILER_MAX_IMAGE_PIXELS so that users can limit the max pixels to value much lower than what PIL support. - Github Issue - https://github.com/django-cms/django-filer/issues/1425 - https://github.com/django-cms/django-filer/issues/1330 Authored-by: Vinit Kumar --- docs/settings.rst | 13 +++++++++++ filer/admin/clipboardadmin.py | 44 +++++++++++++++++++++++++++++++++-- 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/docs/settings.rst b/docs/settings.rst index 9edc82680..4685ce3d8 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -178,6 +178,19 @@ Limits the maximal file size if set. Takes an integer (file size in MB). Defaults to ``None``. +``FILER_MAX_IMAGE_PIXELS`` +-------------------------------- + +Limits the maximal pixel size of the image that can be uploaded to the Filer. +It will also be lower than or equals to the MAX_IMAGE_PIXELS that Pillow's PIL allows. + + +``MAX_IMAGE_PIXELS = int(1024 * 1024 * 1024 // 4 // 3)`` + +Defaults to ``MAX_IMAGE_PIXELS``. But when set, should always be lower than the MAX_IMAGE_PIXELS limit set by Pillow. + +This is useful setting to prevent decompression bomb DOS attack. + ``FILER_ADD_FILE_VALIDATORS`` ----------------------------- diff --git a/filer/admin/clipboardadmin.py b/filer/admin/clipboardadmin.py index b09beb4be..525208c3a 100644 --- a/filer/admin/clipboardadmin.py +++ b/filer/admin/clipboardadmin.py @@ -1,9 +1,11 @@ from django.contrib import admin, messages +from django.conf import settings from django.forms.models import modelform_factory from django.http import JsonResponse from django.urls import path from django.utils.translation import gettext_lazy as _ from django.views.decorators.csrf import csrf_exempt +from PIL.Image import MAX_IMAGE_PIXELS from .. import settings as filer_settings from ..models import Clipboard, ClipboardItem, Folder @@ -20,6 +22,15 @@ ) +# We can never exceed the max pixel value set by Pillow's PIL Image MAX_IMAGE_PIXELS +# as if we allow it, it will fail while thumbnailing (first in the admin thumbnails +# and then in the page itself. +# Refer this https://github.com/python-pillow/Pillow/blob/b723e9e62e4706a85f7e44cb42b3d838dae5e546/src/PIL/Image.py#L3148 +FILER_MAX_IMAGE_PIXELS = min( + getattr(settings, "FILER_MAX_IMAGE_PIXELS", MAX_IMAGE_PIXELS), + MAX_IMAGE_PIXELS, +) + Image = load_model(filer_settings.FILER_IMAGE_MODEL) @@ -124,8 +135,35 @@ def ajax_upload(request, folder_id=None): file_obj = uploadform.save(commit=False) # Enforce the FILER_IS_PUBLIC_DEFAULT file_obj.is_public = filer_settings.FILER_IS_PUBLIC_DEFAULT - file_obj.folder = folder - file_obj.save() + if isinstance(file_obj, Image): + # We check the Image size and calculate the pixel before + # the image gets attached to a folder and saved. We also + # send the error msg in the JSON and also post the message + # so that they know what is wrong with the image they uploaded + from django.contrib.messages import ERROR, add_message + pixels: int = max(1, file_obj.height) * max(1, file_obj.width) + + if pixels > 2 * FILER_MAX_IMAGE_PIXELS: + msg = ( + f"Image size ({pixels} pixels) exceeds limit of {2 * FILER_MAX_IMAGE_PIXELS} " + "pixels, could be decompression bomb DOS attack." + ) + + message = str(msg) + add_message(request, ERROR, message) + return JsonResponse({'error': message}) + + if pixels > FILER_MAX_IMAGE_PIXELS: + msg = f"Image size ({pixels} pixels) exceeds limit of {FILER_MAX_IMAGE_PIXELS} pixels, could be decompression bomb DOS attack." + + message = str(msg) + add_message(request, ERROR, message) + return JsonResponse({'error': message}) + file_obj.folder = folder + file_obj.save() + else: + file_obj.folder = folder + file_obj.save() # TODO: Deprecated/refactor # clipboard_item = ClipboardItem( # clipboard=clipboard, file=file_obj) @@ -140,6 +178,8 @@ def ajax_upload(request, folder_id=None): } # prepare preview thumbnail if isinstance(file_obj, Image): + + thumbnail_180_options = { 'size': (180, 180), 'crop': True,