CVE-2023-44464
Description
pretix before 2023.7.2 allows Pillow to parse EPS files.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
pretix before 2023.7.2 allows uploading EPS files disguised as PNG, triggering Pillow/Ghostscript parsing that can lead to remote code execution.
Vulnerability
pretix, an open-source ticket sales platform, failed to validate that uploaded image files actually match the allowed formats (e.g., PNG, JPEG). By renaming an EPS file to have a .png extension, an attacker could bypass the extension check and have the file parsed by Pillow, which in turn calls Ghostscript to handle EPS data [1][4].
Exploitation
An attacker with permission to upload images—such as event organizers or, if the event enables customer file uploads via the Questions feature, any user—can upload a malicious EPS file. No special authentication is needed beyond the ability to upload images. The processing of the file via Ghostscript can then exploit known vulnerabilities in Ghostscript to achieve remote code execution [2][4].
Impact
Successful exploitation allows an attacker to execute arbitrary code on the server running pretix. This is a critical vulnerability (CVSS score not explicitly given but described as CRITICAL) and can lead to full compromise of the application and underlying system [4].
Mitigation
The vulnerability is patched in pretix version 2023.7.2 and later. Users of the pretix Hosted service had the fix applied automatically. All self-hosted installations should update immediately to the latest version. No workaround is available [1][4].
AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
pretixPyPI | < 2023.7.2 | 2023.7.2 |
Affected products
2- pretix/pretixdescription
Patches
18583bfb7d972[SECURITY] Do not allow Pillow to parse EPS files
13 files changed · +60 −27
src/pretix/api/views/order.py+2 −1 modified@@ -26,6 +26,7 @@ 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, @@ -1191,7 +1192,7 @@ def pdf_image(self, request, key, **kwargs): 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'
src/pretix/base/forms/questions.py+4 −8 modified@@ -500,14 +500,14 @@ def to_python(self, data): 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 @@ def to_python(self, data): 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 @@ def __init__(self, *args, **kwargs): 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:
src/pretix/base/models/orders.py+1 −1 modified@@ -1246,7 +1246,7 @@ def frontend_file_url(self): @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):
src/pretix/base/pdf.py+1 −1 modified@@ -521,7 +521,7 @@ def get_answer(op, order, event, question_id, etag): 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:
src/pretix/base/settings.py+4 −4 modified@@ -2793,7 +2793,7 @@ def unserialize(cls, s): '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 @@ def unserialize(cls, s): '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 @@ def unserialize(cls, s): '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 @@ def unserialize(cls, s): '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.')
src/pretix/control/forms/__init__.py+1 −1 modified@@ -127,7 +127,7 @@ def name(self): @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'):
src/pretix/control/forms/organizer.py+2 −2 modified@@ -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. '
src/pretix/helpers/images.py+2 −1 modified@@ -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:
src/pretix/helpers/monkeypatching.py+16 −0 modified@@ -21,6 +21,8 @@ # from datetime import datetime +from PIL import Image + def monkeypatch_vobject_performance(): """ @@ -52,5 +54,19 @@ def new_tzinfo_eq(tzinfo1, tzinfo2, *args, **kwargs): 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()
src/pretix/helpers/reportlab.py+6 −2 modified@@ -20,8 +20,9 @@ # <https://www.gnu.org/licenses/>. # 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 @@ def resize(self, width, height, dpi): 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 @@ def _jpeg_fh(self): # (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,
src/pretix/helpers/thumb.py+2 −1 modified@@ -23,6 +23,7 @@ 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:
src/pretix/plugins/sendmail/forms.py+1 −5 modified@@ -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
src/pretix/settings.py+18 −0 modified@@ -733,4 +733,22 @@ def traces_sampler(sampling_context): 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
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
7- github.com/advisories/GHSA-9jvx-p6mq-fw4vghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2023-44464ghsaADVISORY
- github.com/pretix/pretix/commit/8583bfb7d97263e9e923ad5d7f123ca1cadc8f2eghsaWEB
- github.com/pretix/pretix/compare/v2023.7.1...v2023.7.2ghsaWEB
- pretix.eu/about/de/blog/20230912-release-2023-7-2ghsaWEB
- pretix.eu/about/en/ticketingghsaWEB
- pretix.eu/about/de/blog/20230912-release-2023-7-2/mitre
News mentions
0No linked articles in our index yet.