Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions apps/api/plane/space/views/intake.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,14 @@ def create(self, request, anchor, intake_id):
status=status.HTTP_400_BAD_REQUEST,
)

# Ensure the intake belongs to this board before writing
# (GHSA-vqr2-wx56-gmq4: caller-supplied intake_id must be bound to the anchor).
if str(intake_id) != str(project_deploy_board.intake_id):
return Response(
{"error": "Intake does not belong to this Project Board"},
status=status.HTTP_400_BAD_REQUEST,
)

if not request.data.get("issue", {}).get("name", False):
return Response({"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST)

Expand Down
51 changes: 51 additions & 0 deletions apps/api/plane/space/views/issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,33 @@
from plane.utils.issue_filters import issue_filters


# GHSA-vqr2-wx56-gmq4: public Spaces board writes must bind caller-supplied
# object ids to the board's project + workspace. Shared by every create() so the
# check can't drift between endpoints or be forgotten on a new one.
def _issue_in_board_scope(issue_id, project_deploy_board):
"""A board-visible issue in the board's project + workspace.

Uses ``issue_objects`` (excludes draft/archived/triage) to match exactly
what the public board displays via ProjectIssuesPublicEndpoint /
IssueRetrievePublicEndpoint — you can only write on what the board shows.
"""
return Issue.issue_objects.filter(
id=issue_id,
project_id=project_deploy_board.project_id,
workspace_id=project_deploy_board.workspace_id,
).exists()


def _comment_in_board_scope(comment_id, project_deploy_board):
"""A public (EXTERNAL) comment in the board's project + workspace."""
return IssueComment.objects.filter(
id=comment_id,
project_id=project_deploy_board.project_id,
workspace_id=project_deploy_board.workspace_id,
access="EXTERNAL",
).exists()


class ProjectIssuesPublicEndpoint(BaseAPIView):
permission_classes = [AllowAny]

Expand Down Expand Up @@ -233,6 +260,7 @@ def get_queryset(self):
super()
.get_queryset()
.filter(workspace_id=project_deploy_board.workspace_id)
.filter(project_id=project_deploy_board.project_id)
.filter(issue_id=self.kwargs.get("issue_id"))
.filter(access="EXTERNAL")
.select_related("project")
Expand Down Expand Up @@ -263,6 +291,10 @@ def create(self, request, anchor, issue_id):
status=status.HTTP_400_BAD_REQUEST,
)

# Bind the caller-supplied issue_id to this board (GHSA-vqr2-wx56-gmq4).
if not _issue_in_board_scope(issue_id, project_deploy_board):
return Response({"error": "Issue not found"}, status=status.HTTP_404_NOT_FOUND)

serializer = IssueCommentSerializer(data=request.data)
if serializer.is_valid():
serializer.save(
Expand Down Expand Up @@ -372,6 +404,10 @@ def create(self, request, anchor, issue_id):
status=status.HTTP_400_BAD_REQUEST,
)

# Bind the caller-supplied issue_id to this board (GHSA-vqr2-wx56-gmq4).
if not _issue_in_board_scope(issue_id, project_deploy_board):
return Response({"error": "Issue not found"}, status=status.HTTP_404_NOT_FOUND)

serializer = IssueReactionSerializer(data=request.data)
if serializer.is_valid():
serializer.save(
Expand Down Expand Up @@ -457,6 +493,10 @@ def create(self, request, anchor, comment_id):
status=status.HTTP_400_BAD_REQUEST,
)

# Bind the caller-supplied comment_id to this board (GHSA-vqr2-wx56-gmq4).
if not _comment_in_board_scope(comment_id, project_deploy_board):
return Response({"error": "Comment not found"}, status=status.HTTP_404_NOT_FOUND)

serializer = CommentReactionSerializer(data=request.data)
if serializer.is_valid():
serializer.save(
Expand Down Expand Up @@ -542,6 +582,17 @@ def get_queryset(self):

def create(self, request, anchor, issue_id):
project_deploy_board = DeployBoard.objects.get(anchor=anchor, entity_name="project")

if not project_deploy_board.is_votes_enabled:
return Response(
{"error": "Votes are not enabled for this project board"},
status=status.HTTP_400_BAD_REQUEST,
)
Comment thread
mguptahub marked this conversation as resolved.

# Bind the caller-supplied issue_id to this board (GHSA-vqr2-wx56-gmq4).
if not _issue_in_board_scope(issue_id, project_deploy_board):
return Response({"error": "Issue not found"}, status=status.HTTP_404_NOT_FOUND)

issue_vote, _ = IssueVote.objects.get_or_create(
actor_id=request.user.id,
project_id=project_deploy_board.project_id,
Expand Down
Loading
Loading