diff --git a/src/pretix/api/views/order.py b/src/pretix/api/views/order.py index bf9ffa3a5d..9928781939 100644 --- a/src/pretix/api/views/order.py +++ b/src/pretix/api/views/order.py @@ -26,6 +26,7 @@ from decimal import Decimal from zoneinfo import ZoneInfo import django_filters +from django.conf import settings from django.db import transaction from django.db.models import ( Exists, F, OuterRef, Prefetch, Q, Subquery, prefetch_related_objects, @@ -1232,7 +1233,7 @@ class OrderPositionViewSet(viewsets.ModelViewSet): ftype, ignored = mimetypes.guess_type(image_file.name) extension = os.path.basename(image_file.name).split('.')[-1] else: - img = Image.open(image_file) + img = Image.open(image_file, formats=settings.PILLOW_FORMATS_QUESTIONS_IMAGE) ftype = Image.MIME[img.format] extensions = { 'GIF': 'gif', 'TIFF': 'tif', 'BMP': 'bmp', 'JPEG': 'jpg', 'PNG': 'png' diff --git a/src/pretix/base/forms/questions.py b/src/pretix/base/forms/questions.py index 6af7b1279b..7af841ed0c 100644 --- a/src/pretix/base/forms/questions.py +++ b/src/pretix/base/forms/questions.py @@ -500,14 +500,14 @@ class PortraitImageField(SizeValidationMixin, ExtValidationMixin, forms.FileFiel file = BytesIO(data['content']) try: - image = Image.open(file) + image = Image.open(file, formats=settings.PILLOW_FORMATS_QUESTIONS_IMAGE) # verify() must be called immediately after the constructor. image.verify() # We want to do more than just verify(), so we need to re-open the file if hasattr(file, 'seek'): file.seek(0) - image = Image.open(file) + image = Image.open(file, formats=settings.PILLOW_FORMATS_QUESTIONS_IMAGE) # load() is a potential DoS vector (see Django bug #18520), so we verify the size first if image.width > 10_000 or image.height > 10_000: @@ -566,7 +566,7 @@ class PortraitImageField(SizeValidationMixin, ExtValidationMixin, forms.FileFiel return f def __init__(self, *args, **kwargs): - kwargs.setdefault('ext_whitelist', (".png", ".jpg", ".jpeg", ".jfif", ".tif", ".tiff", ".bmp")) + kwargs.setdefault('ext_whitelist', settings.FILE_UPLOAD_EXTENSIONS_QUESTION_IMAGE) kwargs.setdefault('max_size', settings.FILE_UPLOAD_MAX_SIZE_IMAGE) super().__init__(*args, **kwargs) @@ -826,11 +826,7 @@ class BaseQuestionsForm(forms.Form): help_text=help_text, initial=initial.file if initial else None, widget=UploadedFileWidget(position=pos, event=event, answer=initial), - ext_whitelist=( - ".png", ".jpg", ".gif", ".jpeg", ".pdf", ".txt", ".docx", ".gif", ".svg", - ".pptx", ".ppt", ".doc", ".xlsx", ".xls", ".jfif", ".heic", ".heif", ".pages", - ".bmp", ".tif", ".tiff" - ), + ext_whitelist=settings.FILE_UPLOAD_EXTENSIONS_OTHER, max_size=settings.FILE_UPLOAD_MAX_SIZE_OTHER, ) elif q.type == Question.TYPE_DATE: diff --git a/src/pretix/base/models/orders.py b/src/pretix/base/models/orders.py index e134a4f527..914b150d20 100644 --- a/src/pretix/base/models/orders.py +++ b/src/pretix/base/models/orders.py @@ -1264,7 +1264,7 @@ class QuestionAnswer(models.Model): @property def is_image(self): - return any(self.file.name.lower().endswith(e) for e in ('.jpg', '.png', '.gif', '.tiff', '.bmp', '.jpeg')) + return any(self.file.name.lower().endswith(e) for e in settings.FILE_UPLOAD_EXTENSIONS_QUESTION_IMAGE) @property def file_name(self): diff --git a/src/pretix/base/pdf.py b/src/pretix/base/pdf.py index bb649887c5..748dd3d6a4 100644 --- a/src/pretix/base/pdf.py +++ b/src/pretix/base/pdf.py @@ -524,7 +524,7 @@ def images_from_questions(sender, *args, **kwargs): else: a = op.answers.filter(question_id=question_id).first() or a - if not a or not a.file or not any(a.file.name.lower().endswith(e) for e in (".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tif", ".tiff")): + if not a or not a.file or not any(a.file.name.lower().endswith(e) for e in settings.FILE_UPLOAD_EXTENSIONS_QUESTION_IMAGE): return None else: if etag: diff --git a/src/pretix/base/settings.py b/src/pretix/base/settings.py index 93ae9af4b9..30794eb8c9 100644 --- a/src/pretix/base/settings.py +++ b/src/pretix/base/settings.py @@ -2793,7 +2793,7 @@ Your {organizer} team""")) # noqa: W291 'form_class': ExtFileField, 'form_kwargs': dict( label=_('Header image'), - ext_whitelist=(".png", ".jpg", ".gif", ".jpeg"), + ext_whitelist=settings.FILE_UPLOAD_EXTENSIONS_IMAGE, max_size=settings.FILE_UPLOAD_MAX_SIZE_IMAGE, help_text=_('If you provide a logo image, we will by default not show your event name and date ' 'in the page header. By default, we show your logo with a size of up to 1140x120 pixels. You ' @@ -2836,7 +2836,7 @@ Your {organizer} team""")) # noqa: W291 'form_class': ExtFileField, 'form_kwargs': dict( label=_('Header image'), - ext_whitelist=(".png", ".jpg", ".gif", ".jpeg"), + ext_whitelist=settings.FILE_UPLOAD_EXTENSIONS_IMAGE, max_size=settings.FILE_UPLOAD_MAX_SIZE_IMAGE, help_text=_('If you provide a logo image, we will by default not show your organization name ' 'in the page header. By default, we show your logo with a size of up to 1140x120 pixels. You ' @@ -2876,7 +2876,7 @@ Your {organizer} team""")) # noqa: W291 'form_class': ExtFileField, 'form_kwargs': dict( label=_('Social media image'), - ext_whitelist=(".png", ".jpg", ".gif", ".jpeg"), + ext_whitelist=settings.FILE_UPLOAD_EXTENSIONS_IMAGE, max_size=settings.FILE_UPLOAD_MAX_SIZE_IMAGE, help_text=_('This picture will be used as a preview if you post links to your ticket shop on social media. ' 'Facebook advises to use a picture size of 1200 x 630 pixels, however some platforms like ' @@ -2897,7 +2897,7 @@ Your {organizer} team""")) # noqa: W291 'form_class': ExtFileField, 'form_kwargs': dict( label=_('Logo image'), - ext_whitelist=(".png", ".jpg", ".gif", ".jpeg"), + ext_whitelist=settings.FILE_UPLOAD_EXTENSIONS_IMAGE, required=False, max_size=settings.FILE_UPLOAD_MAX_SIZE_IMAGE, help_text=_('We will show your logo with a maximal height and width of 2.5 cm.') diff --git a/src/pretix/control/forms/__init__.py b/src/pretix/control/forms/__init__.py index 8810d7ef54..a4a85aae9e 100644 --- a/src/pretix/control/forms/__init__.py +++ b/src/pretix/control/forms/__init__.py @@ -127,7 +127,7 @@ class ClearableBasenameFileInput(forms.ClearableFileInput): @property def is_img(self): - return any(self.file.name.lower().endswith(e) for e in ('.jpg', '.jpeg', '.png', '.gif')) + return any(self.file.name.lower().endswith(e) for e in settings.FILE_UPLOAD_EXTENSIONS_IMAGE) def __str__(self): if hasattr(self.file, 'display_name'): diff --git a/src/pretix/control/forms/organizer.py b/src/pretix/control/forms/organizer.py index efee29375e..e357f442b0 100644 --- a/src/pretix/control/forms/organizer.py +++ b/src/pretix/control/forms/organizer.py @@ -420,7 +420,7 @@ class OrganizerSettingsForm(SettingsForm): organizer_logo_image = ExtFileField( label=_('Header image'), - ext_whitelist=(".png", ".jpg", ".gif", ".jpeg"), + ext_whitelist=settings.FILE_UPLOAD_EXTENSIONS_IMAGE, max_size=settings.FILE_UPLOAD_MAX_SIZE_IMAGE, required=False, help_text=_('If you provide a logo image, we will by default not show your organization name ' @@ -430,7 +430,7 @@ class OrganizerSettingsForm(SettingsForm): ) favicon = ExtFileField( label=_('Favicon'), - ext_whitelist=(".ico", ".png", ".jpg", ".gif", ".jpeg"), + ext_whitelist=settings.FILE_UPLOAD_EXTENSIONS_FAVICON, required=False, max_size=settings.FILE_UPLOAD_MAX_SIZE_FAVICON, help_text=_('If you provide a favicon, we will show it instead of the default pretix icon. ' diff --git a/src/pretix/helpers/images.py b/src/pretix/helpers/images.py index c347bb9f9d..728413c624 100644 --- a/src/pretix/helpers/images.py +++ b/src/pretix/helpers/images.py @@ -22,6 +22,7 @@ import logging from io import BytesIO +from django.conf import settings from django.core.exceptions import ValidationError from django.utils.translation import gettext_lazy as _ from PIL.Image import MAX_IMAGE_PIXELS, DecompressionBombError @@ -51,7 +52,7 @@ def validate_uploaded_file_for_valid_image(f): try: try: - image = Image.open(file) + image = Image.open(file, formats=settings.PILLOW_FORMATS_QUESTIONS_IMAGE) # verify() must be called immediately after the constructor. image.verify() except DecompressionBombError: diff --git a/src/pretix/helpers/monkeypatching.py b/src/pretix/helpers/monkeypatching.py index 44359f3e20..a260343a49 100644 --- a/src/pretix/helpers/monkeypatching.py +++ b/src/pretix/helpers/monkeypatching.py @@ -21,6 +21,8 @@ # from datetime import datetime +from PIL import Image + def monkeypatch_vobject_performance(): """ @@ -52,5 +54,19 @@ def monkeypatch_vobject_performance(): icalendar.tzinfo_eq = new_tzinfo_eq +def monkeypatch_pillow_safer(): + """ + Pillow supports many file formats, among them EPS. For EPS, Pillow loads GhostScript whenever GhostScript + is installed (cannot officially be disabled). However, GhostScript is known for regular security vulnerabilities. + We have no use of reading EPS files and usually prevent this by using `Image.open(…, formats=[…])` to disable EPS + support explicitly. However, we are worried about our dependencies like reportlab using `Image.open` without the + `formats=` parameter. Therefore, as a defense in depth approach, we monkeypatch EPS support away by modifying the + internal image format registry of Pillow. + """ + if "EPS" in Image.ID: + Image.ID.remove("EPS") + + def monkeypatch_all_at_ready(): monkeypatch_vobject_performance() + monkeypatch_pillow_safer() diff --git a/src/pretix/helpers/reportlab.py b/src/pretix/helpers/reportlab.py index 58f92d6b97..b2af459c6c 100644 --- a/src/pretix/helpers/reportlab.py +++ b/src/pretix/helpers/reportlab.py @@ -20,8 +20,9 @@ # . # from arabic_reshaper import ArabicReshaper +from django.conf import settings from django.utils.functional import SimpleLazyObject -from PIL.Image import Resampling +from PIL import Image from reportlab.lib.utils import ImageReader @@ -33,7 +34,7 @@ class ThumbnailingImageReader(ImageReader): height = width * self._image.size[1] / self._image.size[0] self._image.thumbnail( size=(int(width * dpi / 72), int(height * dpi / 72)), - resample=Resampling.BICUBIC + resample=Image.Resampling.BICUBIC ) self._data = None return width, height @@ -44,6 +45,9 @@ class ThumbnailingImageReader(ImageReader): # (smaller) size of the modified image. return None + def _read_image(self, fp): + return Image.open(fp, formats=settings.PILLOW_FORMATS_IMAGE) + reshaper = SimpleLazyObject(lambda: ArabicReshaper(configuration={ 'delete_harakat': True, diff --git a/src/pretix/helpers/thumb.py b/src/pretix/helpers/thumb.py index d93957b5da..157d55510c 100644 --- a/src/pretix/helpers/thumb.py +++ b/src/pretix/helpers/thumb.py @@ -23,6 +23,7 @@ import hashlib import math from io import BytesIO +from django.conf import settings from django.core.files.base import ContentFile from django.core.files.storage import default_storage from PIL import Image, ImageOps, ImageSequence @@ -165,7 +166,7 @@ def resize_image(image, size): def create_thumbnail(sourcename, size): source = default_storage.open(sourcename) - image = Image.open(BytesIO(source.read())) + image = Image.open(BytesIO(source.read()), formats=settings.PILLOW_FORMATS_QUESTIONS_IMAGE) try: image.load() except: diff --git a/src/pretix/plugins/sendmail/forms.py b/src/pretix/plugins/sendmail/forms.py index 45980c5701..4f7fbe466b 100644 --- a/src/pretix/plugins/sendmail/forms.py +++ b/src/pretix/plugins/sendmail/forms.py @@ -76,11 +76,7 @@ class BaseMailForm(FormPlaceholderMixin, forms.Form): attachment = CachedFileField( label=_("Attachment"), required=False, - ext_whitelist=( - ".png", ".jpg", ".gif", ".jpeg", ".pdf", ".txt", ".docx", ".gif", ".svg", - ".pptx", ".ppt", ".doc", ".xlsx", ".xls", ".jfif", ".heic", ".heif", ".pages", - ".bmp", ".tif", ".tiff" - ), + ext_whitelist=settings.FILE_UPLOAD_EXTENSIONS_EMAIL_ATTACHMENT, help_text=_('Sending an attachment increases the chance of your email not arriving or being sorted into spam folders. We recommend only using PDFs ' 'of no more than 2 MB in size.'), max_size=settings.FILE_UPLOAD_MAX_SIZE_EMAIL_ATTACHMENT diff --git a/src/pretix/settings.py b/src/pretix/settings.py index c12fd68c4f..4661534752 100644 --- a/src/pretix/settings.py +++ b/src/pretix/settings.py @@ -735,4 +735,22 @@ FILE_UPLOAD_MAX_SIZE_EMAIL_ATTACHMENT = 1024 * 1024 * config.getint("pretix_file FILE_UPLOAD_MAX_SIZE_EMAIL_AUTO_ATTACHMENT = 1024 * 1024 * config.getint("pretix_file_upload", "max_size_email_auto_attachment", fallback=1) FILE_UPLOAD_MAX_SIZE_OTHER = 1024 * 1024 * config.getint("pretix_file_upload", "max_size_other", fallback=10) +# Allowed file extensions for various places plus matching Pillow formats. +# Never allow EPS, it is full of dangerous bugs. +FILE_UPLOAD_EXTENSIONS_IMAGE = (".png", ".jpg", ".gif", ".jpeg") +PILLOW_FORMATS_IMAGE = ('PNG', 'GIF', 'JPEG') + +FILE_UPLOAD_EXTENSIONS_FAVICON = (".ico", ".png", "jpg", ".gif", ".jpeg") + +FILE_UPLOAD_EXTENSIONS_QUESTION_IMAGE = (".png", "jpg", ".gif", ".jpeg", ".bmp", ".tif", ".tiff", ".jfif") +PILLOW_FORMATS_QUESTIONS_IMAGE = ('PNG', 'GIF', 'JPEG', 'BMP', 'TIFF') + +FILE_UPLOAD_EXTENSIONS_EMAIL_ATTACHMENT = ( + ".png", ".jpg", ".gif", ".jpeg", ".pdf", ".txt", ".docx", ".gif", ".svg", + ".pptx", ".ppt", ".doc", ".xlsx", ".xls", ".jfif", ".heic", ".heif", ".pages", + ".bmp", ".tif", ".tiff" +) +FILE_UPLOAD_EXTENSIONS_OTHER = FILE_UPLOAD_EXTENSIONS_EMAIL_ATTACHMENT + + DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' # sadly. we would prefer BigInt, and should use it for all new models but the migration will be hard