From f3fec39b86b54beeba531729e2f128922c48386d Mon Sep 17 00:00:00 2001 From: Anishka Khurana <25anishkakhurana@gmail.com> Date: Sun, 16 Nov 2025 12:54:07 +0530 Subject: [PATCH 1/3] feat: add rate limiting for email change verification --- website/views/user.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/website/views/user.py b/website/views/user.py index 3988275fa3..16e585c692 100644 --- a/website/views/user.py +++ b/website/views/user.py @@ -11,6 +11,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.models import User from django.contrib.sites.shortcuts import get_current_site +from django.core.cache import cache from django.core.mail import send_mail from django.db.models import Count, F, Q, Sum from django.db.models.functions import ExtractMonth @@ -187,6 +188,18 @@ def profile_edit(request): defaults={"verified": False, "primary": False}, ) + # Rate limit: + rate_key = f"email_verification_rate_{request.user.id}" + if cache.get(rate_key): + messages.error( + request, + "You are doing that too often. Please wait a minute before trying again.", + ) + return redirect("profile", slug=request.user.username) + + # Set limit for 60 seconds + cache.set(rate_key, True, timeout=60) + # Send verification email try: send_email_confirmation(request, request.user, email=new_email) From 89ce42cbcddca04dbf3f6c82e7422ed5bb6d1fd8 Mon Sep 17 00:00:00 2001 From: Anishka Khurana <25anishkakhurana@gmail.com> Date: Sun, 16 Nov 2025 13:28:30 +0530 Subject: [PATCH 2/3] fix: use atomic cache.add() for rate limit --- website/views/user.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/website/views/user.py b/website/views/user.py index 16e585c692..c1b53bb64c 100644 --- a/website/views/user.py +++ b/website/views/user.py @@ -188,18 +188,17 @@ def profile_edit(request): defaults={"verified": False, "primary": False}, ) - # Rate limit: + # Rate limit: atomic check-and-set to prevent race conditions rate_key = f"email_verification_rate_{request.user.id}" - if cache.get(rate_key): + + # add() only sets if key doesn't exist (atomic operation) + if not cache.add(rate_key, True, timeout=60): messages.error( request, "You are doing that too often. Please wait a minute before trying again.", ) return redirect("profile", slug=request.user.username) - # Set limit for 60 seconds - cache.set(rate_key, True, timeout=60) - # Send verification email try: send_email_confirmation(request, request.user, email=new_email) From f8146477504409ea90bebcfba49490950ed98931 Mon Sep 17 00:00:00 2001 From: Anishka Khurana <25anishkakhurana@gmail.com> Date: Sun, 16 Nov 2025 14:39:52 +0530 Subject: [PATCH 3/3] chore: improve UX message and use warning level for rate limit --- website/views/user.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/views/user.py b/website/views/user.py index c1b53bb64c..0cb18c67a9 100644 --- a/website/views/user.py +++ b/website/views/user.py @@ -193,9 +193,9 @@ def profile_edit(request): # add() only sets if key doesn't exist (atomic operation) if not cache.add(rate_key, True, timeout=60): - messages.error( + messages.warning( request, - "You are doing that too often. Please wait a minute before trying again.", + "Too many requests. Please wait a minute before trying again.", ) return redirect("profile", slug=request.user.username)