diff --git a/src/pretix/__init__.py b/src/pretix/__init__.py index 86a2cb9ad..86d132f5e 100644 --- a/src/pretix/__init__.py +++ b/src/pretix/__init__.py @@ -19,4 +19,4 @@ # You should have received a copy of the GNU Affero General Public License along with this program. If not, see # . # -__version__ = "2025.10.0" +__version__ = "2025.10.1" diff --git a/src/pretix/api/views/exporters.py b/src/pretix/api/views/exporters.py index 63344b21e..9cbe4c59f 100644 --- a/src/pretix/api/views/exporters.py +++ b/src/pretix/api/views/exporters.py @@ -74,6 +74,11 @@ class ExportersMixin: @action(detail=True, methods=['GET'], url_name='download', url_path='download/(?P[^/]+)/(?P[^/]+)') 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 @@ class ExportersMixin: 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() diff --git a/src/pretix/base/models/base.py b/src/pretix/base/models/base.py index d8154aa8f..5027c067a 100644 --- a/src/pretix/base/models/base.py +++ b/src/pretix/base/models/base.py @@ -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): diff --git a/src/pretix/base/views/cachedfiles.py b/src/pretix/base/views/cachedfiles.py index 744ea8c1d..487d60863 100644 --- a/src/pretix/base/views/cachedfiles.py +++ b/src/pretix/base/views/cachedfiles.py @@ -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() diff --git a/src/pretix/control/views/modelimport.py b/src/pretix/control/views/modelimport.py index d70171f63..9d5cb8526 100644 --- a/src/pretix/control/views/modelimport.py +++ b/src/pretix/control/views/modelimport.py @@ -38,6 +38,7 @@ from datetime import timedelta 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 @@ class BaseImportView(TemplateView): 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 @@ class BaseProcessView(AsyncAction, FormView): @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): diff --git a/src/pretix/control/views/pdf.py b/src/pretix/control/views/pdf.py index 719fa4cd9..bc2164d99 100644 --- a/src/pretix/control/views/pdf.py +++ b/src/pretix/control/views/pdf.py @@ -247,7 +247,7 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView): 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 diff --git a/src/pretix/control/views/shredder.py b/src/pretix/control/views/shredder.py index aed2c07ab..5236035f0 100644 --- a/src/pretix/control/views/shredder.py +++ b/src/pretix/control/views/shredder.py @@ -38,7 +38,8 @@ from collections import OrderedDict 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 @@ class ShredDownloadView(RecentAuthenticationRequiredMixin, EventPermissionRequir 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 @@ class ShredDownloadView(RecentAuthenticationRequiredMixin, EventPermissionRequir 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