Low severityGHSA Advisory· Published Dec 19, 2025· Updated Apr 15, 2026
CVE-2025-14881
CVE-2025-14881
Description
Multiple API endpoints allowed access to sensitive files from other users by knowing the UUID of the file that were not intended to be accessible by UUID only.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
pretixPyPI | >= 2025.10.0, < 2025.10.1 | 2025.10.1 |
pretixPyPI | >= 2025.9.0, < 2025.9.3 | 2025.9.3 |
pretixPyPI | < 2025.8.3 | 2025.8.3 |
Affected products
1Patches
14b5651862c57[SECURITY] Prevent access to arbitrary cached files by UUID (CVE-2025-14881)
6 files changed · +52 −8
src/pretix/api/views/exporters.py+7 −1 modified@@ -74,6 +74,11 @@ def retrieve(self, request, *args, **kwargs): @action(detail=True, methods=['GET'], url_name='download', url_path='download/(?P<asyncid>[^/]+)/(?P<cfid>[^/]+)') def download(self, *args, **kwargs): cf = get_object_or_404(CachedFile, id=kwargs['cfid']) + if not cf.allowed_for_session(self.request, "exporters-api"): + return Response( + {'status': 'failed', 'message': 'Unknown file ID or export failed'}, + status=status.HTTP_410_GONE + ) if cf.file: resp = ChunkBasedFileResponse(cf.file.file, content_type=cf.type) resp['Content-Disposition'] = 'attachment; filename="{}"'.format(cf.filename).encode("ascii", "ignore") @@ -109,7 +114,8 @@ def run(self, *args, **kwargs): serializer = JobRunSerializer(exporter=instance, data=self.request.data, **self.get_serializer_kwargs()) serializer.is_valid(raise_exception=True) - cf = CachedFile(web_download=False) + cf = CachedFile(web_download=True) + cf.bind_to_session(self.request, "exporters-api") cf.date = now() cf.expires = now() + timedelta(hours=24) cf.save()
src/pretix/base/models/base.py+31 −0 modified@@ -58,6 +58,37 @@ class CachedFile(models.Model): web_download = models.BooleanField(default=True) # allow web download, True for backwards compatibility in plugins session_key = models.TextField(null=True, blank=True) # only allow download in this session + def session_key_for_request(self, request, salt=None): + from ...api.models import OAuthAccessToken, OAuthApplication + from .devices import Device + from .organizer import TeamAPIToken + + if hasattr(request, "auth") and isinstance(request.auth, OAuthAccessToken): + k = f'app:{request.auth.application.pk}' + elif hasattr(request, "auth") and isinstance(request.auth, OAuthApplication): + k = f'app:{request.auth.pk}' + elif hasattr(request, "auth") and isinstance(request.auth, TeamAPIToken): + k = f'token:{request.auth.pk}' + elif hasattr(request, "auth") and isinstance(request.auth, Device): + k = f'device:{request.auth.pk}' + elif request.session.session_key: + k = request.session.session_key + else: + raise ValueError("No auth method found to bind to") + + if salt: + k = f"{k}!{salt}" + return k + + def allowed_for_session(self, request, salt=None): + return ( + not self.session_key or + self.session_key_for_request(request, salt) == self.session_key + ) + + def bind_to_session(self, request, salt=None): + self.session_key = self.session_key_for_request(request, salt) + @receiver(post_delete, sender=CachedFile) def cached_file_delete(sender, instance, **kwargs):
src/pretix/base/views/cachedfiles.py+2 −3 modified@@ -36,9 +36,8 @@ class DownloadView(TemplateView): def object(self) -> CachedFile: try: o = get_object_or_404(CachedFile, id=self.kwargs['id'], web_download=True) - if o.session_key: - if o.session_key != self.request.session.session_key: - raise Http404() + if not o.allowed_for_session(self.request): + raise Http404() return o except (ValueError, ValidationError): # Invalid URLs raise Http404()
src/pretix/control/views/modelimport.py+6 −1 modified@@ -38,6 +38,7 @@ from django.conf import settings from django.contrib import messages +from django.http import Http404 from django.shortcuts import get_object_or_404, redirect from django.urls import reverse from django.utils.functional import cached_property @@ -85,6 +86,7 @@ def post(self, request, *args, **kwargs): filename='import.csv', type='text/csv', ) + cf.bind_to_session(request, "modelimport") cf.file.save('import.csv', request.FILES['file']) if self.request.POST.get("charset") in ENCODINGS: @@ -137,7 +139,10 @@ def form_valid(self, form): @cached_property def file(self): - return get_object_or_404(CachedFile, pk=self.kwargs.get("file"), filename="import.csv") + cf = get_object_or_404(CachedFile, pk=self.kwargs.get("file"), filename="import.csv") + if not cf.allowed_for_session(self.request, "modelimport"): + raise Http404() + return cf @cached_property def parsed(self):
src/pretix/control/views/pdf.py+1 −1 modified@@ -247,7 +247,7 @@ def post(self, request, *args, **kwargs): cf = None if request.POST.get("background", "").strip(): try: - cf = CachedFile.objects.get(id=request.POST.get("background")) + cf = CachedFile.objects.get(id=request.POST.get("background"), web_download=True) except CachedFile.DoesNotExist: pass
src/pretix/control/views/shredder.py+5 −2 modified@@ -38,7 +38,8 @@ from zipfile import ZipFile from django.contrib import messages -from django.shortcuts import get_object_or_404, redirect +from django.http import Http404 +from django.shortcuts import redirect from django.urls import reverse from django.utils.functional import cached_property from django.utils.translation import get_language, gettext_lazy as _ @@ -94,6 +95,8 @@ def get_context_data(self, **kwargs): cf = CachedFile.objects.get(pk=kwargs['file']) except CachedFile.DoesNotExist: raise ShredError(_("The download file could no longer be found on the server, please try to start again.")) + if not cf.allowed_for_session(self.request): + raise Http404() with ZipFile(cf.file.file, 'r') as zipfile: indexdata = json.loads(zipfile.read('index.json').decode()) @@ -111,7 +114,7 @@ def get_context_data(self, **kwargs): ctx = super().get_context_data(**kwargs) ctx['shredders'] = self.shredders ctx['download_on_shred'] = any(shredder.require_download_confirmation for shredder in shredders) - ctx['file'] = get_object_or_404(CachedFile, pk=kwargs.get("file")) + ctx['file'] = cf return ctx
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5News mentions
0No linked articles in our index yet.