diff --git a/src/pretix/api/auth/utils.py b/src/pretix/api/auth/utils.py new file mode 100644 index 0000000000..0d6e574e8c --- /dev/null +++ b/src/pretix/api/auth/utils.py @@ -0,0 +1,9 @@ +def get_session_key_for_api_auth(user, auth): + if user.is_authenticated: + return f'api-upload-User-{user.pk}' + else: + return f'api-upload-{str(type(auth))}-{auth.pk}' + + +def get_session_key_for_api_request(request): + return get_session_key_for_api_auth(request.user, request.auth) diff --git a/src/pretix/api/serializers/fields.py b/src/pretix/api/serializers/fields.py index d877028ab7..dce9fc0e69 100644 --- a/src/pretix/api/serializers/fields.py +++ b/src/pretix/api/serializers/fields.py @@ -37,6 +37,8 @@ from collections import OrderedDict from django.core.exceptions import ValidationError from rest_framework import serializers +from pretix.api.auth.utils import get_session_key_for_api_request + def remove_duplicates_from_list(data): return list(OrderedDict.fromkeys(data)) @@ -83,10 +85,16 @@ class UploadedFileField(serializers.Field): request = self.context.get('request', None) try: cf = CachedFile.objects.get( - session_key=f'api-upload-{str(type(request.user or request.auth))}-{(request.user or request.auth).pk}', file__isnull=False, pk=data[len("file:"):], ) + if cf.session_key == "api-upload--None": + # OK, backwards-compatibility of a security bug fixed 2026-09, delete this at some point, but should + # also be harmless because all files with this key are expired one day after deployment of this fix + # and no new files with this key are created + pass + elif cf.session_key != get_session_key_for_api_request(request): + self.fail('not_found') except (ValidationError, IndexError): # invalid uuid self.fail('not_found') except CachedFile.DoesNotExist: diff --git a/src/pretix/api/serializers/order.py b/src/pretix/api/serializers/order.py index 9fa6b5ce92..404526e8b4 100644 --- a/src/pretix/api/serializers/order.py +++ b/src/pretix/api/serializers/order.py @@ -41,6 +41,7 @@ from rest_framework.exceptions import ValidationError from rest_framework.relations import SlugRelatedField from rest_framework.reverse import reverse +from pretix.api.auth.utils import get_session_key_for_api_request from pretix.api.serializers import CompatibleJSONField from pretix.api.serializers.event import SubEventSerializer from pretix.api.serializers.forms import form_field_to_serializer_field @@ -257,16 +258,21 @@ class AnswerSerializer(I18nAwareModelSerializer): if data['answer'] == 'file:keep': return data try: - ao = self.context["request"].user or self.context["request"].auth cf = CachedFile.objects.get( - session_key=f'api-upload-{str(type(ao))}-{ao.pk}', file__isnull=False, pk=data['answer'][len("file:"):], ) + if cf.session_key == "api-upload--None": + # OK, backwards-compatibility of a security bug fixed 2026-09, delete this at some point, but should + # also be harmless because all files with this key are expired one day after deployment of this fix + # and no new files with this key are created + pass + elif cf.session_key != get_session_key_for_api_request(self.context["request"]): + raise ValidationError('The submitted file ID "{fid}" was not found.'.format(fid=data['answer'])) except (ValidationError, IndexError): # invalid uuid - raise ValidationError('The submitted file ID "{fid}" was not found.'.format(fid=data)) + raise ValidationError('The submitted file ID "{fid}" was not found.'.format(fid=data['answer'])) except CachedFile.DoesNotExist: - raise ValidationError('The submitted file ID "{fid}" was not found.'.format(fid=data)) + raise ValidationError('The submitted file ID "{fid}" was not found.'.format(fid=data['answer'])) allowed_types = ( 'image/png', 'image/jpeg', 'image/gif', 'application/pdf' diff --git a/src/pretix/api/views/checkin.py b/src/pretix/api/views/checkin.py index c6567a4d5c..706a0b3c6e 100644 --- a/src/pretix/api/views/checkin.py +++ b/src/pretix/api/views/checkin.py @@ -50,6 +50,7 @@ from rest_framework.generics import ListAPIView from rest_framework.permissions import SAFE_METHODS from rest_framework.response import Response +from pretix.api.auth.utils import get_session_key_for_api_auth from pretix.api.serializers.checkin import ( CheckinListSerializer, CheckinRPCAnnulInputSerializer, CheckinRPCRedeemInputSerializer, MiniCheckinListSerializer, @@ -327,10 +328,16 @@ with scopes_disabled(): def _handle_file_upload(data, user, auth): try: cf = CachedFile.objects.get( - session_key=f'api-upload-{str(type(user or auth))}-{(user or auth).pk}', file__isnull=False, pk=data[len("file:"):], ) + if cf.session_key == "api-upload--None": + # OK, backwards-compatibility of a security bug fixed 2026-09, delete this at some point, but should + # also be harmless because all files with this key are expired one day after deployment of this fix + # and no new files with this key are created + pass + elif cf.session_key != get_session_key_for_api_auth(user, auth): + raise ValidationError('The submitted file ID "{fid}" was not found.'.format(fid=data)) except (ValidationError, BaseValidationError, IndexError): # invalid uuid raise ValidationError('The submitted file ID "{fid}" was not found.'.format(fid=data)) except CachedFile.DoesNotExist: diff --git a/src/pretix/api/views/upload.py b/src/pretix/api/views/upload.py index e6426de647..bad09c4f3b 100644 --- a/src/pretix/api/views/upload.py +++ b/src/pretix/api/views/upload.py @@ -33,6 +33,7 @@ from rest_framework.views import APIView from pretix.api.auth.device import DeviceTokenAuthentication from pretix.api.auth.permission import AnyAuthenticatedClientPermission from pretix.api.auth.token import TeamTokenAuthentication +from pretix.api.auth.utils import get_session_key_for_api_request from pretix.base.models import CachedFile from pretix.helpers.images import ( IMAGE_TYPES, validate_uploaded_file_for_valid_image, @@ -78,7 +79,7 @@ class UploadView(APIView): web_download=False, filename=file_obj.name, type=content_type, - session_key=f'api-upload-{str(type(request.user or request.auth))}-{(request.user or request.auth).pk}' + session_key=get_session_key_for_api_request(request) ) cf.file.save(file_obj.name, file_obj) cf.save()