Password reset: Prevent race condition that uses token twice

This commit is contained in:
Raphael Michel
2026-08-04 09:52:07 +02:00
parent 7627e4b548
commit ba45a8b476
2 changed files with 23 additions and 9 deletions
+12 -9
View File
@@ -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')
+11
View File
@@ -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()