From 162ffb1cb934d073f712c28b70c0ccd559c30077 Mon Sep 17 00:00:00 2001 From: Manish Gupta Date: Tue, 23 Jun 2026 16:42:23 +0530 Subject: [PATCH 1/2] fix: enforce workspace membership on entity-search endpoint (GHSA-32q3-mqpc-3mhv) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SearchEndpoint required authentication but did not verify the requesting user was a member of the queried workspace. Any authenticated Plane user could enumerate members across workspaces they don't belong to by guessing slugs. Add a WorkspaceMember guard at the top of get() — returns 403 if the user is not an active member of the target workspace. Brings OSS to parity with EE, which already had this protection via @can(WorkspacePermissions.VIEW). Co-authored-by: Plane AI --- apps/api/plane/app/views/search/base.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/apps/api/plane/app/views/search/base.py b/apps/api/plane/app/views/search/base.py index 3bfbecaaff0..ec5bf4156a2 100644 --- a/apps/api/plane/app/views/search/base.py +++ b/apps/api/plane/app/views/search/base.py @@ -303,6 +303,16 @@ def get(self, request, slug): class SearchEndpoint(BaseAPIView): def get(self, request, slug): + # Verify the requesting user is an active member of the target workspace. + # Without this guard any authenticated user can enumerate members of + # workspaces they do not belong to (GHSA-32q3-mqpc-3mhv). + if not WorkspaceMember.objects.filter( + member=request.user, + workspace__slug=slug, + is_active=True, + ).exists(): + return Response({"error": "Forbidden"}, status=status.HTTP_403_FORBIDDEN) + query = request.query_params.get("query", False) query_types = request.query_params.get("query_type", "user_mention").split(",") query_types = [qt.strip() for qt in query_types] From 233a96deec5996f38c450afb8046141484968bf5 Mon Sep 17 00:00:00 2001 From: Manish Gupta Date: Wed, 24 Jun 2026 12:54:14 +0530 Subject: [PATCH 2/2] refactor(security): replace inline WS membership check with WorkspaceUserPermission Use the existing WorkspaceUserPermission permission class on SearchEndpoint instead of a manual WorkspaceMember.objects.filter() guard inside the method body. Enforcement behaviour is unchanged (GHSA-32q3-mqpc-3mhv). Co-authored-by: Plane AI --- apps/api/plane/app/views/search/base.py | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/apps/api/plane/app/views/search/base.py b/apps/api/plane/app/views/search/base.py index ec5bf4156a2..1aff9d6c750 100644 --- a/apps/api/plane/app/views/search/base.py +++ b/apps/api/plane/app/views/search/base.py @@ -28,6 +28,7 @@ # Module imports from plane.app.views.base import BaseAPIView +from plane.app.permissions import WorkspaceUserPermission from plane.db.models import ( Workspace, Project, @@ -302,17 +303,9 @@ def get(self, request, slug): class SearchEndpoint(BaseAPIView): - def get(self, request, slug): - # Verify the requesting user is an active member of the target workspace. - # Without this guard any authenticated user can enumerate members of - # workspaces they do not belong to (GHSA-32q3-mqpc-3mhv). - if not WorkspaceMember.objects.filter( - member=request.user, - workspace__slug=slug, - is_active=True, - ).exists(): - return Response({"error": "Forbidden"}, status=status.HTTP_403_FORBIDDEN) + permission_classes = (WorkspaceUserPermission,) + def get(self, request, slug): query = request.query_params.get("query", False) query_types = request.query_params.get("query_type", "user_mention").split(",") query_types = [qt.strip() for qt in query_types]