From d2f8b6858810efb059bfb5198b2e0b42bd496c38 Mon Sep 17 00:00:00 2001 From: Manish Gupta Date: Wed, 24 Jun 2026 14:39:18 +0530 Subject: [PATCH] fix(security): prevent project invite email disclosure via unauthenticated GET ProjectJoinEndpoint.get() was AllowAny and used ProjectMemberInviteSerializer (fields = "__all__"), leaking the invitee's email and token to anyone who knew the workspace slug, project ID, and invite UUID (GHSA-2r58-hgv7-635q). Introduce ProjectMemberInvitePublicSerializer with an explicit safe field list that excludes `email` and `token`, and swap it in for the public GET endpoint. The full serializer is retained for authenticated admin viewsets. Co-authored-by: Plane AI --- apps/api/plane/app/serializers/__init__.py | 1 + apps/api/plane/app/serializers/project.py | 25 ++++++++++++++++++++++ apps/api/plane/app/views/project/invite.py | 7 ++++-- 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/apps/api/plane/app/serializers/__init__.py b/apps/api/plane/app/serializers/__init__.py index 75cd35afc3d..6de6ee89870 100644 --- a/apps/api/plane/app/serializers/__init__.py +++ b/apps/api/plane/app/serializers/__init__.py @@ -35,6 +35,7 @@ ProjectDetailSerializer, ProjectMemberSerializer, ProjectMemberInviteSerializer, + ProjectMemberInvitePublicSerializer, ProjectIdentifierSerializer, ProjectLiteSerializer, ProjectMemberLiteSerializer, diff --git a/apps/api/plane/app/serializers/project.py b/apps/api/plane/app/serializers/project.py index 924c48fcfa3..aef296bc6c2 100644 --- a/apps/api/plane/app/serializers/project.py +++ b/apps/api/plane/app/serializers/project.py @@ -203,6 +203,31 @@ class Meta: fields = "__all__" +class ProjectMemberInvitePublicSerializer(BaseSerializer): + """Safe read-only serializer for the public project invite GET endpoint. + + Intentionally excludes ``email`` and ``token`` so that an unauthenticated + caller cannot retrieve the invitee's email address or the acceptance token + (GHSA-2r58-hgv7-635q). + """ + + project = ProjectLiteSerializer(read_only=True) + workspace = WorkspaceLiteSerializer(read_only=True) + + class Meta: + model = ProjectMemberInvite + fields = [ + "id", + "project", + "workspace", + "role", + "message", + "accepted", + "responded_at", + ] + read_only_fields = fields + + class ProjectIdentifierSerializer(BaseSerializer): class Meta: model = ProjectIdentifier diff --git a/apps/api/plane/app/views/project/invite.py b/apps/api/plane/app/views/project/invite.py index 19d8c36bcf7..bc78c1a1c19 100644 --- a/apps/api/plane/app/views/project/invite.py +++ b/apps/api/plane/app/views/project/invite.py @@ -19,7 +19,10 @@ # Module imports from .base import BaseViewSet, BaseAPIView -from plane.app.serializers import ProjectMemberInviteSerializer +from plane.app.serializers import ( + ProjectMemberInviteSerializer, + ProjectMemberInvitePublicSerializer, +) from plane.app.permissions import allow_permission, ROLE from plane.db.models import ( ProjectMember, @@ -250,5 +253,5 @@ def post(self, request, slug, project_id, pk): def get(self, request, slug, project_id, pk): project_invitation = ProjectMemberInvite.objects.get(workspace__slug=slug, project_id=project_id, pk=pk) - serializer = ProjectMemberInviteSerializer(project_invitation) + serializer = ProjectMemberInvitePublicSerializer(project_invitation) return Response(serializer.data, status=status.HTTP_200_OK)