From 7f38f3708e09d1d0bbae16f315d080a25a1c6aa8 Mon Sep 17 00:00:00 2001 From: Manish Gupta Date: Thu, 25 Jun 2026 15:49:19 +0530 Subject: [PATCH] [WEB-7892] fix(security): scope attachment PATCH/DELETE/GET by issue_id, drop created_by overwrite (GHSA-5mxw-g5mw-3v3w) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All three V2 issue attachment handlers (PATCH, DELETE, GET single) looked up FileAsset by (pk, workspace, project_id) only — issue_id in the URL was silently ignored. Any project member could target another user's attachment UUID using their own issue_id, and PATCH would transfer ownership via unconditional created_by = request.user. Add issue_id=issue_id to all three FileAsset.objects.get() calls so the lookup is correctly scoped to the attachment's owning issue. Remove the created_by overwrite in PATCH — created_by is set at creation time and must not be reassigned by a subsequent upload-confirm call. Co-authored-by: Plane AI --- apps/api/plane/app/views/issue/attachment.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/apps/api/plane/app/views/issue/attachment.py b/apps/api/plane/app/views/issue/attachment.py index 51248b8a428..c4e7f0b0c89 100644 --- a/apps/api/plane/app/views/issue/attachment.py +++ b/apps/api/plane/app/views/issue/attachment.py @@ -148,7 +148,9 @@ def post(self, request, slug, project_id, issue_id): @allow_permission([ROLE.ADMIN], creator=True, model=FileAsset) def delete(self, request, slug, project_id, issue_id, pk): - issue_attachment = FileAsset.objects.get(pk=pk, workspace__slug=slug, project_id=project_id) + issue_attachment = FileAsset.objects.get( + pk=pk, workspace__slug=slug, project_id=project_id, issue_id=issue_id + ) issue_attachment.is_deleted = True issue_attachment.deleted_at = timezone.now() issue_attachment.save() @@ -171,7 +173,7 @@ def delete(self, request, slug, project_id, issue_id, pk): def get(self, request, slug, project_id, issue_id, pk=None): if pk: # Get the asset - asset = FileAsset.objects.get(id=pk, workspace__slug=slug, project_id=project_id) + asset = FileAsset.objects.get(id=pk, workspace__slug=slug, project_id=project_id, issue_id=issue_id) # Check if the asset is uploaded if not asset.is_uploaded: @@ -202,7 +204,9 @@ def get(self, request, slug, project_id, issue_id, pk=None): @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) def patch(self, request, slug, project_id, issue_id, pk): - issue_attachment = FileAsset.objects.get(pk=pk, workspace__slug=slug, project_id=project_id) + issue_attachment = FileAsset.objects.get( + pk=pk, workspace__slug=slug, project_id=project_id, issue_id=issue_id + ) serializer = IssueAttachmentSerializer(issue_attachment) # Send this activity only if the attachment is not uploaded before @@ -219,9 +223,9 @@ def patch(self, request, slug, project_id, issue_id, pk): origin=base_host(request=request, is_app=True), ) - # Update the attachment + # Update the attachment — do NOT overwrite created_by; it is set at + # creation time and must not be reassigned (GHSA-5mxw-g5mw-3v3w). issue_attachment.is_uploaded = True - issue_attachment.created_by = request.user # Get the storage metadata if not issue_attachment.storage_metadata: