Skip to content

Commit

Permalink
[SECURITY] Do not allow Pillow to parse EPS files
Browse files Browse the repository at this point in the history
  • Loading branch information
raphaelm committed Sep 12, 2023
1 parent 34d1d3f commit 8583bfb
Show file tree
Hide file tree
Showing 13 changed files with 60 additions and 27 deletions.
3 changes: 2 additions & 1 deletion src/pretix/api/views/order.py
Expand Up @@ -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,
Expand Down Expand Up @@ -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'
Expand Down
12 changes: 4 additions & 8 deletions src/pretix/base/forms/questions.py
Expand Up @@ -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:
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion src/pretix/base/models/orders.py
Expand Up @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion src/pretix/base/pdf.py
Expand Up @@ -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:
Expand Down
8 changes: 4 additions & 4 deletions src/pretix/base/settings.py
Expand Up @@ -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 '
Expand Down Expand Up @@ -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 '
Expand Down Expand Up @@ -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 '
Expand All @@ -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.')
Expand Down
2 changes: 1 addition & 1 deletion src/pretix/control/forms/__init__.py
Expand Up @@ -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'):
Expand Down
4 changes: 2 additions & 2 deletions src/pretix/control/forms/organizer.py
Expand Up @@ -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 '
Expand All @@ -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. '
Expand Down
3 changes: 2 additions & 1 deletion src/pretix/helpers/images.py
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
16 changes: 16 additions & 0 deletions src/pretix/helpers/monkeypatching.py
Expand Up @@ -21,6 +21,8 @@
#
from datetime import datetime

from PIL import Image


def monkeypatch_vobject_performance():
"""
Expand Down Expand Up @@ -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()
8 changes: 6 additions & 2 deletions src/pretix/helpers/reportlab.py
Expand Up @@ -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


Expand All @@ -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
Expand All @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion src/pretix/helpers/thumb.py
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
6 changes: 1 addition & 5 deletions src/pretix/plugins/sendmail/forms.py
Expand Up @@ -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
Expand Down
18 changes: 18 additions & 0 deletions src/pretix/settings.py
Expand Up @@ -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

0 comments on commit 8583bfb

Please sign in to comment.