diff --git a/src/pretix/control/views/auth.py b/src/pretix/control/views/auth.py index cf1f8c3442..bbbf380242 100644 --- a/src/pretix/control/views/auth.py +++ b/src/pretix/control/views/auth.py @@ -64,6 +64,7 @@ from pretix.base.forms.auth import ( ) from pretix.base.metrics import pretix_failed_logins, pretix_successful_logins from pretix.base.models import TeamInvite, U2FDevice, User, WebAuthnDevice +from pretix.helpers import OF_SELF from pretix.helpers.http import get_client_ip, redirect_to_url from pretix.helpers.ratelimit import rate_limit, rate_limit_reset from pretix.helpers.security import handle_login_source, session_login @@ -395,15 +396,17 @@ class Recover(TemplateView): def post(self, request, *args, **kwargs): if self.form.is_valid(): - try: - user = User.objects.get(id=self.request.GET.get('id'), auth_backend='native') - except User.DoesNotExist: - return self.invalid('unknownuser') - if not default_token_generator.check_token(user, self.request.GET.get('token')): - return self.invalid('invalid') - user.set_password(self.form.cleaned_data['password']) - user.needs_password_change = False - user.save() + with transaction.atomic(): + # Check token in transaction to prevent race condition + try: + user = User.objects.select_for_update(of=OF_SELF).get(id=self.request.GET.get('id'), auth_backend='native') + except User.DoesNotExist: + return self.invalid('unknownuser') + if not default_token_generator.check_token(user, self.request.GET.get('token')): + return self.invalid('invalid') + user.set_password(self.form.cleaned_data['password']) + user.needs_password_change = False + user.save() messages.success(request, _('You can now login using your new password.')) user.log_action('pretix.control.auth.user.forgot_password.recovered') diff --git a/src/pretix/presale/views/customer.py b/src/pretix/presale/views/customer.py index 71a3c2012b..bd4c3196c9 100644 --- a/src/pretix/presale/views/customer.py +++ b/src/pretix/presale/views/customer.py @@ -54,6 +54,7 @@ from pretix.base.models import Customer, InvoiceAddress, Order, OrderPosition from pretix.base.services.mail import mail from pretix.base.settings import PERSON_NAME_SCHEMES from pretix.base.signals import customer_created, customer_signed_in +from pretix.helpers import OF_SELF from pretix.helpers.compat import CompatDeleteView from pretix.helpers.http import redirect_to_url from pretix.multidomain.models import KnownDomain @@ -280,6 +281,16 @@ class SetPasswordView(FormView): def form_valid(self, form): with transaction.atomic(): + # Re-check token in transaction to prevent race condition + try: + self.customer = Customer.objects.select_for_update(of=OF_SELF).get(pk=self.customer.pk) + except Customer.DoesNotExist: + messages.error(self.request, _('You clicked an invalid link.')) + else: + if not TokenGenerator().check_token(self.customer, self.request.GET.get('token', '')): + messages.error(self.request, _('You clicked an invalid link.')) + return HttpResponseRedirect(self.get_success_url()) + self.customer.set_password(form.cleaned_data['password']) self.customer.is_verified = True self.customer.save()