Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@Tia-ani
Copy link
Contributor

@Tia-ani Tia-ani commented Nov 16, 2025

Fixes : #4840

Summary

This PR introduces a rate-limiting mechanism to the email verification step in
the profile edit flow. It is a follow-up to PR #4804, adding an
extra safety improvement recommended by the maintainers.

What this PR adds

  • A simple per-user rate limit using Django’s cache framework
  • Prevents sending multiple verification emails within 60 seconds
  • Clear and user-friendly error message when the rate limit is reached
  • Clean and minimal code changes to keep behavior consistent

Why this improvement was needed

Email systems can be abused by repeatedly triggering verification emails.
Adding rate-limiting aligns the behavior with common best practices and ensures
a smoother user experience.

How it works

  • A cache key is created based on the user ID:
    email_verification_rate_<user_id>
  • If the key exists → show a friendly message asking the user to wait
  • If not → send the verification email and set the key with a 60-second expiry

Additional notes

This PR builds upon the previous merged improvement and follows the project’s
existing structure and coding patterns.

Summary by CodeRabbit

  • Improvements
    • Email verification requests are now rate-limited: users can only resend verification emails once per minute. If the limit is exceeded, a warning is shown and the user is redirected to the profile page.

@github-actions
Copy link
Contributor

👋 Hi @Tia-ani!

This pull request needs a peer review before it can be merged. Please request a review from a team member who is not:

  • The PR author
  • DonnieBLT
  • coderabbit
  • copilot

Once a valid peer review is submitted, this check will pass automatically. Thank you!

@github-actions github-actions bot added unresolved-conversations: 0 PR has 0 unresolved conversations files-changed: 1 labels Nov 16, 2025
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 16, 2025

Walkthrough

Adds a per-user, cache-backed rate limiter to the profile_edit email verification flow: attempts an atomic cache.add with a 60-second key; if the key exists the request is rejected with a warning and redirect, otherwise the verification email is sent and the key is set.

Changes

Cohort / File(s) Summary
Rate-limiting for email verification
website/views/user.py
Added cache import and implemented per-user rate-limiting in profile_edit using an atomic cache.add on key email_verification_rate_<user_id> with 60s TTL; on cache hit the view shows a warning and redirects instead of sending the email.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant profile_edit as profile_edit()
    participant Cache

    User->>profile_edit: POST request to trigger email verification
    profile_edit->>Cache: cache.add(email_verification_rate_<user_id>, True, 60)
    
    alt cache.add returned False (rate limit active)
        Cache-->>profile_edit: False
        profile_edit-->>User: Show warning + redirect to profile
    else cache.add returned True (allowed)
        Cache-->>profile_edit: True
        profile_edit->>profile_edit: Send verification email
        profile_edit-->>User: Continue normal flow
    end
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

  • Verify cache.add usage is atomic and compatible with configured cache backend.
  • Confirm correct cache key construction and 60s TTL is intended.
  • Check warning message text and redirect target in profile_edit.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: adding rate limiting to email verification in the profile update flow, which matches the implementation of cache-based rate limiting with a 60-second timeout.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
website/views/user.py (1)

191-209: Rate limit applied before email send blocks retry on legitimate failures.

The rate limit is set at line 201 before attempting to send the email (lines 204-209). If send_email_confirmation() fails due to network issues, SMTP errors, or other transient problems, the user is still rate-limited for 60 seconds and cannot immediately retry. This creates a poor user experience when failures are legitimate.

Apply this diff to set the rate limit only after successful email send:

                 # Rate limit: atomic check-and-set to prevent race conditions
                 rate_key = f"email_verification_rate_{request.user.id}"
-                # add() only sets if key doesn't exist (atomic operation)
-                if not cache.add(rate_key, True, timeout=60):
+                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)
 
                 # Send verification email
                 try:
                     send_email_confirmation(request, request.user, email=new_email)
+                    # Set rate limit only after successful send (atomic operation)
+                    cache.add(rate_key, True, timeout=60)
                 except Exception as e:
                     logger.exception(f"Failed to send email confirmation to {new_email}: {e}")
                     messages.error(request, "Failed to send verification email. Please try again later.")
                     return redirect("profile", slug=request.user.username)

Note: This combined with the atomic cache.add() from the previous comment resolves both the race condition and the retry-blocking issue.

🧹 Nitpick comments (1)
website/views/user.py (1)

191-201: Consider using a dedicated rate limiting library.

While the current cache-based implementation works, consider using a well-tested library like django-ratelimit for more robust rate limiting with better handling of edge cases, distributed systems support, and configurable backends.

Example with django-ratelimit:

First, install the package:

pip install django-ratelimit

Then apply this refactor:

+from django_ratelimit.decorators import ratelimit
+from django_ratelimit.exceptions import Ratelimited

 @login_required
+@ratelimit(key='user', rate='1/60s', method='POST', block=False)
 def profile_edit(request):
     from allauth.account.models import EmailAddress
     from allauth.account.utils import send_email_confirmation
 
     Tag.objects.get_or_create(name="GSOC")
     user_profile, created = UserProfile.objects.get_or_create(user=request.user)
 
     # Get the user's current email BEFORE changes
     original_email = request.user.email
 
     if request.method == "POST":
+        # Check if rate limited
+        if getattr(request, 'limited', False):
+            messages.error(
+                request,
+                "You are doing that too often. Please wait a minute before trying again.",
+            )
+            return redirect("profile", slug=request.user.username)
+
         form = UserProfileForm(request.POST, request.FILES, instance=user_profile)
 
         if form.is_valid():
             ...
             if email_changed:
                 # Remove any pending unverified emails
                 EmailAddress.objects.filter(user=request.user, verified=False).delete()
 
                 # Create new unverified email entry
                 EmailAddress.objects.update_or_create(
                     user=request.user,
                     email=new_email,
                     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)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Knowledge base: Disabled due to Reviews -> Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between cc20d8b and f3fec39.

📒 Files selected for processing (1)
  • website/views/user.py (2 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Run Tests
  • GitHub Check: docker-test
🔇 Additional comments (2)
website/views/user.py (2)

14-14: LGTM! Cache import added correctly.

The Django cache import is properly placed and follows standard conventions.


191-201: The rate limiting cache backend is properly configured and does not use DummyCache.

The codebase uses LocMemCache (functional in-memory cache) across all deployment scenarios and falls back to PyLibMCCache (Memcached) when configured. There is no scenario where DummyCache (the silent-failure backend) is used. Rate limiting will function correctly.

Likely an incorrect or invalid review comment.

@github-project-automation github-project-automation bot moved this from Backlog to Ready in 📌 OWASP BLT Project Board Nov 16, 2025
@github-actions github-actions bot added unresolved-conversations: 1 PR has 1 unresolved conversation and removed unresolved-conversations: 0 PR has 0 unresolved conversations labels Nov 16, 2025
@github-actions github-actions bot added unresolved-conversations: 0 PR has 0 unresolved conversations and removed unresolved-conversations: 1 PR has 1 unresolved conversation labels Nov 16, 2025
@sidd190
Copy link
Contributor

sidd190 commented Nov 16, 2025

LGTM.

Just a nitpick, maybe use messages.warning for a bit of UX improvement overall and change the message to "Too many requests. Please wait a minute before trying again" instead if it makes sense.

@Tia-ani
Copy link
Contributor Author

Tia-ani commented Nov 16, 2025

Thank you for reviewing @sidd190!
Great suggestion — using messages.warning and a cleaner “Too many requests…” message makes sense. I’ll update the PR with that improvement.

@sidd190
Copy link
Contributor

sidd190 commented Nov 16, 2025

LGTM.

Implements rate limiting to the previously merged PR.

@Tia-ani Tia-ani requested a review from DonnieBLT November 16, 2025 17:15
@Tia-ani
Copy link
Contributor Author

Tia-ani commented Nov 16, 2025

Hi @DonnieBLT ! I accidentally clicked “Request re-review”
No updates were made after your approval, so the previous review still stands.
Sorry for the confusion.

@DonnieBLT DonnieBLT merged commit 433ae62 into OWASP-BLT:main Nov 16, 2025
24 of 27 checks passed
@Tia-ani Tia-ani deleted the fix/email-verification-rate-limit branch November 17, 2025 05:51
@DonnieBLT
Copy link
Collaborator

/tip @Tia-ani $1

@github-actions
Copy link
Contributor

💰 Tip Request from @DonnieBLT to @Tia-ani

Amount: $1

To complete this tip, please visit @Tia-ani's GitHub Sponsors page and select a one-time payment:

🔗 Sponsor @Tia-ani

Note: GitHub Sponsors does not support automated payments via API. Please complete the transaction manually by selecting "One-time" on the sponsor page and entering your desired amount.


This comment was generated by OWASP BLT-Action

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

files-changed: 1 unresolved-conversations: 0 PR has 0 unresolved conversations

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Add rate-limiting when requesting email verification during profile update

3 participants