From c2c2df3d21667ec39c20b3bbbba65b15d2470a8f Mon Sep 17 00:00:00 2001 From: sriramveeraghanta Date: Mon, 4 May 2026 15:49:11 +0530 Subject: [PATCH] fix(api): scope cross-workspace resource lookups to prevent IDOR `ProjectViewSet.partial_update`, `BulkEstimatePointEndpoint.partial_update`, and `WorkspaceUserProfileEndpoint.get` previously fetched objects by primary key alone after a workspace-scoped permission check, allowing an authenticated caller to act on resources belonging to other workspaces by supplying a foreign UUID with their own workspace slug in the URL. - Project partial_update: scope `Project.objects.get` by `workspace__slug`, matching the existing pattern in `destroy`. - Bulk estimate partial_update: scope `Estimate.objects.get` by `workspace__slug` and `project_id`, matching `retrieve` and `destroy`. - Workspace user profile: require the target `user_id` to be an active member of the requested workspace before returning email and other PII. --- apps/api/plane/app/views/estimate/base.py | 2 +- apps/api/plane/app/views/project/base.py | 2 +- apps/api/plane/app/views/workspace/user.py | 9 +++++++-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/apps/api/plane/app/views/estimate/base.py b/apps/api/plane/app/views/estimate/base.py index 4bdc86633d6..46d5f7a6b6a 100644 --- a/apps/api/plane/app/views/estimate/base.py +++ b/apps/api/plane/app/views/estimate/base.py @@ -113,7 +113,7 @@ def partial_update(self, request, slug, project_id, estimate_id): status=status.HTTP_400_BAD_REQUEST, ) - estimate = Estimate.objects.get(pk=estimate_id) + estimate = Estimate.objects.get(pk=estimate_id, workspace__slug=slug, project_id=project_id) if request.data.get("estimate"): estimate.name = request.data.get("estimate").get("name", estimate.name) diff --git a/apps/api/plane/app/views/project/base.py b/apps/api/plane/app/views/project/base.py index 0a7378c076f..57489943fef 100644 --- a/apps/api/plane/app/views/project/base.py +++ b/apps/api/plane/app/views/project/base.py @@ -332,7 +332,7 @@ def partial_update(self, request, slug, pk=None): workspace = Workspace.objects.get(slug=slug) - project = Project.objects.get(pk=pk) + project = Project.objects.get(pk=pk, workspace__slug=slug) intake_view = request.data.get("inbox_view", project.intake_view) current_instance = json.dumps(ProjectSerializer(project).data, cls=DjangoJSONEncoder) if project.archived_at: diff --git a/apps/api/plane/app/views/workspace/user.py b/apps/api/plane/app/views/workspace/user.py index b60ae5e15eb..5b028a32ffb 100644 --- a/apps/api/plane/app/views/workspace/user.py +++ b/apps/api/plane/app/views/workspace/user.py @@ -279,11 +279,16 @@ def get(self, request, slug): class WorkspaceUserProfileEndpoint(BaseAPIView): def get(self, request, slug, user_id): - user_data = User.objects.get(pk=user_id) - requesting_workspace_member = WorkspaceMember.objects.get( workspace__slug=slug, member=request.user, is_active=True ) + + # Verify the target user is also an active member of this workspace + # before exposing their profile data. + target_workspace_member = WorkspaceMember.objects.select_related("member").get( + workspace__slug=slug, member_id=user_id, is_active=True + ) + user_data = target_workspace_member.member projects = [] if requesting_workspace_member.role >= 15: projects = (