diff --git a/apps/api/plane/app/views/project/invite.py b/apps/api/plane/app/views/project/invite.py index 19d8c36bcf7..33673a1acc2 100644 --- a/apps/api/plane/app/views/project/invite.py +++ b/apps/api/plane/app/views/project/invite.py @@ -186,22 +186,45 @@ class ProjectJoinEndpoint(BaseAPIView): def post(self, request, slug, project_id, pk): project_invite = ProjectMemberInvite.objects.get(pk=pk, project_id=project_id, workspace__slug=slug) - email = request.data.get("email", "") + token = request.data.get("token", "") - if email == "" or project_invite.email != email: + # Validate the token to verify the user received the invitation email + if not token or project_invite.token != token: return Response( {"error": "You do not have permission to join the project"}, status=status.HTTP_403_FORBIDDEN, ) + # Require an authenticated session — the accepting user must be the + # person who was invited. Without this check an attacker who knows the + # invitee email and obtains the token can hijack the project membership + # (GHSA-g36h-p63v-g9c7). + if not request.user.is_authenticated: + return Response( + {"error": "Authentication required to accept project invitation"}, + status=status.HTTP_401_UNAUTHORIZED, + ) + if request.user.email.lower() != project_invite.email.lower(): + return Response( + {"error": "You do not have permission to accept this invitation"}, + status=status.HTTP_403_FORBIDDEN, + ) + if project_invite.responded_at is None: - project_invite.accepted = request.data.get("accepted", False) + accepted = request.data.get("accepted", False) + if not isinstance(accepted, bool): + return Response( + {"error": "`accepted` must be a boolean"}, + status=status.HTTP_400_BAD_REQUEST, + ) + project_invite.accepted = accepted project_invite.responded_at = timezone.now() project_invite.save() if project_invite.accepted: - # Check if the user account exists - user = User.objects.filter(email=email).first() + # Use the authenticated user directly — they've already been + # validated as the invite recipient above. + user = request.user # Check if user is a part of workspace workspace_member = WorkspaceMember.objects.filter(workspace__slug=slug, member=user).first()