From 6c9dbb50434d16ea00a00d1574723dfd0c3d2446 Mon Sep 17 00:00:00 2001 From: Manish Gupta <59428681+mguptahub@users.noreply.github.com> Date: Tue, 23 Jun 2026 18:10:40 +0530 Subject: [PATCH] [WEB-7787] fix(security): block deactivated user login and fix WorkspaceOwnerPermission (#9290) GHSA-rmmf-rj2q-3rrg: save_user_data() was unconditionally setting is_active=True on every login, silently reactivating any admin-deactivated account. Fix: add an early guard in complete_login_or_signup() that raises USER_ACCOUNT_DEACTIVATED (5019) before any session or save logic if the existing user's is_active=False. Remove the is_active=True assignment and the associated user_activation_email call from save_user_data(). Also remove the now-unused user_activation_email and base_host imports. GHSA-wjgv-cq7w-258v: WorkspaceOwnerPermission in both app/permissions/ and utils/permissions/ was filtering WorkspaceMember without is_active=True, allowing a deactivated workspace owner/admin to retain API access. Add is_active=True to both copies to match every other permission class. Co-authored-by: Plane AI --- apps/api/plane/app/permissions/workspace.py | 2 +- apps/api/plane/authentication/adapter/base.py | 18 ++++++++++-------- apps/api/plane/utils/permissions/workspace.py | 2 +- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/apps/api/plane/app/permissions/workspace.py b/apps/api/plane/app/permissions/workspace.py index ada16ec3b5..d979e608f9 100644 --- a/apps/api/plane/app/permissions/workspace.py +++ b/apps/api/plane/app/permissions/workspace.py @@ -54,7 +54,7 @@ class WorkspaceOwnerPermission(BasePermission): return False return WorkspaceMember.objects.filter( - workspace__slug=view.workspace_slug, member=request.user, role=Admin + workspace__slug=view.workspace_slug, member=request.user, role=Admin, is_active=True ).exists() diff --git a/apps/api/plane/authentication/adapter/base.py b/apps/api/plane/authentication/adapter/base.py index 1b906c4a8d..b1520b1ec1 100644 --- a/apps/api/plane/authentication/adapter/base.py +++ b/apps/api/plane/authentication/adapter/base.py @@ -19,14 +19,11 @@ from django.utils import timezone # Third party imports from zxcvbn import zxcvbn -from plane.bgtasks.user_activation_email_task import user_activation_email - # Module imports from plane.db.models import FileAsset, Profile, User, WorkspaceMemberInvite from plane.license.utils.instance_value import get_configuration_value from plane.settings.storage import S3Storage from plane.utils.exception_logger import log_exception -from plane.utils.host import base_host from plane.utils.ip_address import get_client_ip from .error import AUTHENTICATION_ERROR_CODES, AuthenticationException @@ -239,11 +236,6 @@ class Adapter: user.last_login_ip = get_client_ip(request=self.request) user.last_login_uagent = self.request.META.get("HTTP_USER_AGENT") user.token_updated_at = timezone.now() - # If user is not active, send the activation email and set the user as active - if not user.is_active: - user_activation_email.delay(base_host(request=self.request), user.id) - # Set user as active - user.is_active = True user.save() return user @@ -309,6 +301,16 @@ class Adapter: # Check if the user is present user = User.objects.filter(email=email).first() + + # Reject deactivated accounts before any session or save logic. + # Without this check, save_user_data() would reactivate the account (GHSA-rmmf-rj2q-3rrg). + if user and not user.is_active: + raise AuthenticationException( + error_code=AUTHENTICATION_ERROR_CODES["USER_ACCOUNT_DEACTIVATED"], + error_message="USER_ACCOUNT_DEACTIVATED", + payload={"email": email}, + ) + # Check if sign up case or login is_signup = bool(user) # If user is not present, create a new user diff --git a/apps/api/plane/utils/permissions/workspace.py b/apps/api/plane/utils/permissions/workspace.py index ada16ec3b5..d979e608f9 100644 --- a/apps/api/plane/utils/permissions/workspace.py +++ b/apps/api/plane/utils/permissions/workspace.py @@ -54,7 +54,7 @@ class WorkspaceOwnerPermission(BasePermission): return False return WorkspaceMember.objects.filter( - workspace__slug=view.workspace_slug, member=request.user, role=Admin + workspace__slug=view.workspace_slug, member=request.user, role=Admin, is_active=True ).exists()