Compare commits

...

9 Commits

Author SHA1 Message Date
pajowu
f74de683e2 Fix typo
Co-authored-by: Raphael Michel <michel@pretix.eu>
2026-04-08 15:56:44 +02:00
Kara Engelhardt
a7418a75a5 Remove unneccesary csrf_protect decorators 2026-03-30 10:46:26 +02:00
Kara Engelhardt
31a1e35071 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.
2026-03-26 11:15:03 +01:00
Kara Engelhardt
77eb6eb23b Fix tests 2026-03-25 13:34:46 +01:00
Richard Schreiber
58d6d7f71b fix typo 2026-03-25 12:21:50 +01:00
Richard Schreiber
43917c62a8 fix isort 2026-03-25 12:21:50 +01:00
Richard Schreiber
44e010a207 fix debug typo 2026-03-25 12:21:50 +01:00
Richard Schreiber
38e69d1e32 change to helper delete_cookie_without_samesite 2026-03-25 12:21:50 +01:00
Richard Schreiber
777b504bbe Fix delete_cookie for partitioned legacy CSRF cookie 2026-03-25 12:21:50 +01:00
4 changed files with 71 additions and 22 deletions

View File

@@ -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=""):

View File

@@ -20,6 +20,7 @@
# <https://www.gnu.org/licenses/>.
#
import re
from datetime import datetime
from django.conf import settings
@@ -48,6 +49,10 @@ def set_cookie_without_samesite(request, response, key, *args, **kwargs):
response.cookies[key]['Partitioned'] = True
def delete_cookie_without_samesite(request, response, key, *args, **kwargs):
kwargs['expires'] = datetime.fromtimestamp(0).strftime("%a, %d %b %Y %H:%M:%S GMT")
set_cookie_without_samesite(request, response, key, *args, **kwargs)
# Based on https://www.chromium.org/updates/same-site/incompatible-clients
# Copyright 2019 Google LLC.
# SPDX-License-Identifier: Apache-2.0

View File

@@ -50,12 +50,15 @@ 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
from pretix.base.models import Event, Organizer
from pretix.helpers.cookies import set_cookie_without_samesite
from pretix.helpers.cookies import (
delete_cookie_without_samesite, set_cookie_without_samesite,
)
from pretix.multidomain.models import KnownDomain
LOCAL_HOST_NAMES = ('testserver', 'localhost')
@@ -176,9 +179,23 @@ class SessionMiddleware(BaseSessionMiddleware):
# The session should be deleted only if the session is entirely empty
is_secure = request.scheme == 'https'
if '__Host-' + settings.SESSION_COOKIE_NAME in request.COOKIES and empty:
response.delete_cookie('__Host-' + settings.SESSION_COOKIE_NAME)
# response.delete_cookie does not work as we might have set a partitioned cookie
delete_cookie_without_samesite(
request, response,
'__Host-' + settings.SESSION_COOKIE_NAME,
path=settings.SESSION_COOKIE_PATH,
secure=is_secure,
httponly=settings.SESSION_COOKIE_HTTPONLY or None
)
elif settings.SESSION_COOKIE_NAME in request.COOKIES and empty:
response.delete_cookie(settings.SESSION_COOKIE_NAME)
# response.delete_cookie does not work as we might have set a partitioned cookie
delete_cookie_without_samesite(
request, response,
settings.SESSION_COOKIE_NAME,
path=settings.SESSION_COOKIE_PATH,
secure=is_secure,
httponly=settings.SESSION_COOKIE_HTTPONLY or None
)
else:
if accessed:
patch_vary_headers(response, ('Cookie',))
@@ -195,15 +212,21 @@ class SessionMiddleware(BaseSessionMiddleware):
if response.status_code != 500:
request.session.save()
if is_secure and settings.SESSION_COOKIE_NAME in request.COOKIES: # remove legacy cookie
response.delete_cookie(settings.SESSION_COOKIE_NAME)
response.delete_cookie(settings.SESSION_COOKIE_NAME, samesite="None")
# response.delete_cookie does not work as we might have set a partitioned cookie
delete_cookie_without_samesite(
request, response,
settings.SESSION_COOKIE_NAME,
path=settings.SESSION_COOKIE_PATH,
secure=is_secure,
httponly=settings.SESSION_COOKIE_HTTPONLY or None
)
set_cookie_without_samesite(
request, response,
'__Host-' + settings.SESSION_COOKIE_NAME if is_secure else settings.SESSION_COOKIE_NAME,
request.session.session_key, max_age=max_age,
expires=expires,
path=settings.SESSION_COOKIE_PATH,
secure=request.scheme == 'https',
secure=is_secure,
httponly=settings.SESSION_COOKIE_HTTPONLY or None
)
return response
@@ -248,20 +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
response.delete_cookie(settings.CSRF_COOKIE_NAME)
response.delete_cookie(settings.CSRF_COOKIE_NAME, samesite="None")
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=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)

View File

@@ -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: