From 4b52dce76e8aa97a8d87166fa8f4441c1cf646b1 Mon Sep 17 00:00:00 2001 From: Manish Gupta <59428681+mguptahub@users.noreply.github.com> Date: Wed, 1 Jul 2026 17:34:45 +0530 Subject: [PATCH] [WEB-7855] fix(security): prevent project invite email disclosure via unauthenticated GET (#9305) 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 75cd35afc3..6de6ee8987 100644 --- a/apps/api/plane/app/serializers/__init__.py +++ b/apps/api/plane/app/serializers/__init__.py @@ -35,6 +35,7 @@ from .project import ( 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 924c48fcfa..aef296bc6c 100644 --- a/apps/api/plane/app/serializers/project.py +++ b/apps/api/plane/app/serializers/project.py @@ -203,6 +203,31 @@ class ProjectMemberInviteSerializer(BaseSerializer): 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 19d8c36bcf..bc78c1a1c1 100644 --- a/apps/api/plane/app/views/project/invite.py +++ b/apps/api/plane/app/views/project/invite.py @@ -19,7 +19,10 @@ from rest_framework.permissions import AllowAny # 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 @@ class ProjectJoinEndpoint(BaseAPIView): 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)