From 4b73990d274291474e12397d408d3caa56c0fa4e Mon Sep 17 00:00:00 2001 From: pajowu Date: Thu, 9 Apr 2026 13:22:40 +0200 Subject: [PATCH] Fix customer views using wrong csrf middleware (#6027) * Fix customer views using wrong csrf middleware This lead to persistent csrf validation errors if the token from the cookie expired, which could only be solved by clearing cookies. * Remove unneccesary csrf_protect decorators * Fix typo Co-authored-by: Raphael Michel --------- Co-authored-by: Raphael Michel --- src/pretix/base/views/errors.py | 2 +- src/pretix/multidomain/middlewares.py | 35 +++++++++++++++++++++++---- src/pretix/presale/views/customer.py | 9 ------- 3 files changed, 31 insertions(+), 15 deletions(-) diff --git a/src/pretix/base/views/errors.py b/src/pretix/base/views/errors.py index 7b76973b9..dbc92e20a 100644 --- a/src/pretix/base/views/errors.py +++ b/src/pretix/base/views/errors.py @@ -27,11 +27,11 @@ from django.template import TemplateDoesNotExist, loader from django.template.loader import get_template from django.utils.functional import Promise from django.utils.translation import gettext as _ -from django.views.decorators.csrf import requires_csrf_token from sentry_sdk import last_event_id from pretix.base.i18n import language from pretix.base.middleware import get_language_from_request +from pretix.multidomain.middlewares import requires_csrf_token def csrf_failure(request, reason=""): diff --git a/src/pretix/multidomain/middlewares.py b/src/pretix/multidomain/middlewares.py index 8207270af..3fb0326d4 100644 --- a/src/pretix/multidomain/middlewares.py +++ b/src/pretix/multidomain/middlewares.py @@ -50,6 +50,7 @@ from django.middleware.csrf import ( from django.shortcuts import render from django.urls import set_urlconf from django.utils.cache import patch_vary_headers +from django.utils.decorators import decorator_from_middleware from django.utils.deprecation import MiddlewareMixin from django.utils.http import http_date from django_scopes import scopes_disabled @@ -270,26 +271,50 @@ class CsrfViewMiddleware(BaseCsrfMiddleware): if request.session.get(CSRF_SESSION_KEY) != request.META["CSRF_COOKIE"]: request.session[CSRF_SESSION_KEY] = request.META["CSRF_COOKIE"] else: - is_secure = request.scheme == 'https' # Set the CSRF cookie even if it's already set, so we renew # the expiry timer. - if is_secure and settings.CSRF_COOKIE_NAME in request.COOKIES: # remove legacy cookie + if request.is_secure() and settings.CSRF_COOKIE_NAME in request.COOKIES: # remove legacy cookie # response.delete_cookie does not work as we might have set a partitioned cookie delete_cookie_without_samesite( request, response, settings.CSRF_COOKIE_NAME, path=settings.CSRF_COOKIE_PATH, - secure=is_secure, + secure=request.is_secure(), httponly=settings.CSRF_COOKIE_HTTPONLY ) set_cookie_without_samesite( request, response, - '__Host-' + settings.CSRF_COOKIE_NAME if is_secure else settings.CSRF_COOKIE_NAME, + '__Host-' + settings.CSRF_COOKIE_NAME if request.is_secure() else settings.CSRF_COOKIE_NAME, request.META["CSRF_COOKIE"], max_age=settings.CSRF_COOKIE_AGE, path=settings.CSRF_COOKIE_PATH, - secure=is_secure, + secure=request.is_secure(), httponly=settings.CSRF_COOKIE_HTTPONLY ) # Content varies with the CSRF cookie, so set the Vary header. patch_vary_headers(response, ('Cookie',)) + + def process_response(self, request, response): + if ( + not settings.CSRF_USE_SESSIONS + and request.is_secure() + and settings.CSRF_COOKIE_NAME in response.cookies + and response.cookies[settings.CSRF_COOKIE_NAME].value + ): + raise ValueError("Usage of djangos CsrfViewMiddleware detected (legacy cookie found in response). " + "This may be caused by using csrf_project or requires_csrf_token from django.views.decorators.csrf. " + "Use the pretix.multidomain.middlewares equivalent instead.") + + return super().process_response(request, response) + + +csrf_protect = decorator_from_middleware(CsrfViewMiddleware) + + +class _EnsureCsrfToken(CsrfViewMiddleware): + # Behave like CsrfViewMiddleware but don't reject requests or log warnings. + def _reject(self, request, reason): + return None + + +requires_csrf_token = decorator_from_middleware(_EnsureCsrfToken) diff --git a/src/pretix/presale/views/customer.py b/src/pretix/presale/views/customer.py index 0919bea23..63ea274aa 100644 --- a/src/pretix/presale/views/customer.py +++ b/src/pretix/presale/views/customer.py @@ -42,7 +42,6 @@ from django.utils.functional import cached_property from django.utils.http import url_has_allowed_host_and_scheme from django.utils.translation import gettext_lazy as _ from django.views.decorators.cache import never_cache -from django.views.decorators.csrf import csrf_protect from django.views.decorators.debug import sensitive_post_parameters from django.views.generic import FormView, ListView, View @@ -99,7 +98,6 @@ class LoginView(RedirectBackMixin, FormView): redirect_authenticated_user = True @method_decorator(sensitive_post_parameters()) - @method_decorator(csrf_protect) @method_decorator(never_cache) def dispatch(self, request, *args, **kwargs): if not request.organizer.settings.customer_accounts: @@ -211,7 +209,6 @@ class RegistrationView(RedirectBackMixin, FormView): redirect_authenticated_user = True @method_decorator(sensitive_post_parameters()) - @method_decorator(csrf_protect) @method_decorator(never_cache) def dispatch(self, request, *args, **kwargs): if not request.organizer.settings.customer_accounts: @@ -255,7 +252,6 @@ class SetPasswordView(FormView): template_name = 'pretixpresale/organizers/customer_setpassword.html' @method_decorator(sensitive_post_parameters()) - @method_decorator(csrf_protect) @method_decorator(never_cache) def dispatch(self, request, *args, **kwargs): if not request.organizer.settings.customer_accounts: @@ -299,7 +295,6 @@ class ResetPasswordView(FormView): template_name = 'pretixpresale/organizers/customer_resetpw.html' @method_decorator(sensitive_post_parameters()) - @method_decorator(csrf_protect) @method_decorator(never_cache) def dispatch(self, request, *args, **kwargs): if not request.organizer.settings.customer_accounts: @@ -523,7 +518,6 @@ class ChangePasswordView(CustomerAccountBaseMixin, FormView): form_class = ChangePasswordForm @method_decorator(sensitive_post_parameters()) - @method_decorator(csrf_protect) @method_decorator(never_cache) def dispatch(self, request, *args, **kwargs): if not request.organizer.settings.customer_accounts: @@ -557,7 +551,6 @@ class ChangeInformationView(CustomerAccountBaseMixin, FormView): form_class = ChangeInfoForm @method_decorator(sensitive_post_parameters()) - @method_decorator(csrf_protect) @method_decorator(never_cache) def dispatch(self, request, *args, **kwargs): if not request.organizer.settings.customer_accounts: @@ -665,7 +658,6 @@ class SSOLoginView(RedirectBackMixin, View): redirect_authenticated_user = True @method_decorator(sensitive_post_parameters()) - @method_decorator(csrf_protect) @method_decorator(never_cache) def dispatch(self, request, *args, **kwargs): if not request.organizer.settings.customer_accounts: @@ -728,7 +720,6 @@ class SSOLoginReturnView(RedirectBackMixin, View): redirect_authenticated_user = True @method_decorator(sensitive_post_parameters()) - @method_decorator(csrf_protect) @method_decorator(never_cache) def dispatch(self, request, *args, **kwargs): if not request.organizer.settings.customer_accounts: