From 8d51f700062dfc2fa035e312929599b2a125d8bb Mon Sep 17 00:00:00 2001 From: Manish Gupta Date: Tue, 30 Jun 2026 12:05:19 +0530 Subject: [PATCH] fix: add rate limiting to email/password sign-in and sign-up endpoints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All four password authentication views (app sign-in, app sign-up, space sign-in, space sign-up) extended django.views.View, so DRF's global AnonRateThrottle never ran and the endpoints accepted unlimited credential guesses with no friction (brute-force / credential stuffing, GHSA-349j). Add authentication_throttle_allows(request) at the top of each post() method — before any DB access — using the same AuthenticationThrottle already guarding the magic-code views. On rejection the view redirects with RATE_LIMIT_EXCEEDED, consistent with all other throttled auth endpoints. Default limit remains 10/minute, overridable via AUTHENTICATION_RATE_LIMIT. Co-authored-by: Plane AI --- .../plane/authentication/views/app/email.py | 32 +++++++++++++++++++ .../plane/authentication/views/space/email.py | 29 +++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/apps/api/plane/authentication/views/app/email.py b/apps/api/plane/authentication/views/app/email.py index 3d1954875c4..95d2a6c3dce 100644 --- a/apps/api/plane/authentication/views/app/email.py +++ b/apps/api/plane/authentication/views/app/email.py @@ -11,6 +11,7 @@ # Module imports from plane.authentication.provider.credentials.email import EmailProvider from plane.authentication.utils.login import user_login +from plane.authentication.rate_limit import authentication_throttle_allows from plane.license.models import Instance from plane.authentication.utils.host import base_host from plane.authentication.utils.redirection_path import get_redirection_path @@ -26,6 +27,23 @@ class SignInAuthEndpoint(View): def post(self, request): next_path = request.POST.get("next_path") + + # Rate-limit all password auth attempts before any DB access. + # authentication_throttle_allows() applies the same AuthenticationThrottle + # that magic-code views use (default: 10/minute, configurable via + # AUTHENTICATION_RATE_LIMIT env var). + if not authentication_throttle_allows(request): + exc = AuthenticationException( + error_code=AUTHENTICATION_ERROR_CODES["RATE_LIMIT_EXCEEDED"], + error_message="RATE_LIMIT_EXCEEDED", + ) + url = get_safe_redirect_url( + base_url=base_host(request=request, is_app=True), + next_path=next_path, + params=exc.get_error_dict(), + ) + return HttpResponseRedirect(url) + # Check instance configuration instance = Instance.objects.first() if instance is None or not instance.is_setup_done: @@ -135,6 +153,20 @@ def post(self, request): class SignUpAuthEndpoint(View): def post(self, request): next_path = request.POST.get("next_path") + + # Rate-limit before any DB access (same throttle as sign-in / magic-code). + if not authentication_throttle_allows(request): + exc = AuthenticationException( + error_code=AUTHENTICATION_ERROR_CODES["RATE_LIMIT_EXCEEDED"], + error_message="RATE_LIMIT_EXCEEDED", + ) + url = get_safe_redirect_url( + base_url=base_host(request=request, is_app=True), + next_path=next_path, + params=exc.get_error_dict(), + ) + return HttpResponseRedirect(url) + # Check instance configuration instance = Instance.objects.first() if instance is None or not instance.is_setup_done: diff --git a/apps/api/plane/authentication/views/space/email.py b/apps/api/plane/authentication/views/space/email.py index 827348cef23..ee6b3de0e14 100644 --- a/apps/api/plane/authentication/views/space/email.py +++ b/apps/api/plane/authentication/views/space/email.py @@ -12,6 +12,7 @@ # Module imports from plane.authentication.provider.credentials.email import EmailProvider from plane.authentication.utils.login import user_login +from plane.authentication.rate_limit import authentication_throttle_allows from plane.license.models import Instance from plane.authentication.utils.host import base_host from plane.db.models import User @@ -25,6 +26,20 @@ class SignInAuthSpaceEndpoint(View): def post(self, request): next_path = request.POST.get("next_path") + + # Rate-limit before any DB access. + if not authentication_throttle_allows(request): + exc = AuthenticationException( + error_code=AUTHENTICATION_ERROR_CODES["RATE_LIMIT_EXCEEDED"], + error_message="RATE_LIMIT_EXCEEDED", + ) + url = get_safe_redirect_url( + base_url=base_host(request=request, is_space=True), + next_path=next_path, + params=exc.get_error_dict(), + ) + return HttpResponseRedirect(url) + # Check instance configuration instance = Instance.objects.first() if instance is None or not instance.is_setup_done: @@ -110,6 +125,20 @@ def post(self, request): class SignUpAuthSpaceEndpoint(View): def post(self, request): next_path = request.POST.get("next_path") + + # Rate-limit before any DB access. + if not authentication_throttle_allows(request): + exc = AuthenticationException( + error_code=AUTHENTICATION_ERROR_CODES["RATE_LIMIT_EXCEEDED"], + error_message="RATE_LIMIT_EXCEEDED", + ) + url = get_safe_redirect_url( + base_url=base_host(request=request, is_space=True), + next_path=next_path, + params=exc.get_error_dict(), + ) + return HttpResponseRedirect(url) + # Check instance configuration instance = Instance.objects.first() if instance is None or not instance.is_setup_done: