From 1e10df7dce90dd74d102f9582788578fcfc08f7f Mon Sep 17 00:00:00 2001 From: Manish Gupta Date: Mon, 22 Jun 2026 13:14:49 +0530 Subject: [PATCH] [WEB-7769] fix(security): scope EstimatePoint create/destroy to workspace and project MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GHSA-933r-rxg8-f3h2 — EstimatePointEndpoint.create trusted the estimate_id URL parameter without verifying it belonged to the caller's workspace and project. An authenticated user in project A could inject estimate points into any other workspace's estimate by supplying a foreign estimate_id. Fix: added a workspace+project scoped Estimate ownership check before EstimatePoint.objects.create(). GHSA-933r-rxg8-f3h2 (destroy) — old_estimate_point was fetched with pk only (unscoped), allowing cross-tenant key disclosure and manipulation during the key-rearrangement step. Fix: scoped the old_estimate_point lookup to estimate_id + project_id + workspace__slug; added 404 guard for missing/foreign points. Note: BulkEstimatePointEndpoint.partial_update (GHSA-vm3j-5j49-gwrf) was already correctly scoped at lines 116 and 125-130 — no change needed. Co-authored-by: Plane AI --- apps/api/plane/app/views/estimate/base.py | 25 +++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/apps/api/plane/app/views/estimate/base.py b/apps/api/plane/app/views/estimate/base.py index 46d5f7a6b6a..de1d47cf9e4 100644 --- a/apps/api/plane/app/views/estimate/base.py +++ b/apps/api/plane/app/views/estimate/base.py @@ -159,6 +159,17 @@ def create(self, request, slug, project_id, estimate_id): {"error": "Key and value are required"}, status=status.HTTP_400_BAD_REQUEST, ) + # Verify the estimate belongs to this workspace and project before creating a point + estimate = Estimate.objects.filter( + pk=estimate_id, + workspace__slug=slug, + project_id=project_id, + ).first() + if not estimate: + return Response( + {"error": "Estimate not found"}, + status=status.HTTP_404_NOT_FOUND, + ) key = request.data.get("key", 0) value = request.data.get("value", "") estimate_point = EstimatePoint.objects.create( @@ -227,8 +238,18 @@ def destroy(self, request, slug, project_id, estimate_id, estimate_point_id): epoch=int(timezone.now().timestamp()), ) - # delete the estimate point - old_estimate_point = EstimatePoint.objects.filter(pk=estimate_point_id).first() + # delete the estimate point — scope to this estimate/project/workspace to prevent cross-tenant key manipulation + old_estimate_point = EstimatePoint.objects.filter( + pk=estimate_point_id, + estimate_id=estimate_id, + project_id=project_id, + workspace__slug=slug, + ).first() + if not old_estimate_point: + return Response( + {"error": "Estimate point not found"}, + status=status.HTTP_404_NOT_FOUND, + ) # rearrange the estimate points updated_estimate_points = []