Skip to content
38 changes: 32 additions & 6 deletions apps/api/plane/space/views/issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,9 @@ def get_queryset(self):
super()
.get_queryset()
.filter(workspace_id=project_deploy_board.workspace_id)
# FIX VULN-01: scope comments to the board's project so callers cannot
# read comments from a different project by supplying a foreign issue_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 +266,20 @@ def create(self, request, anchor, issue_id):
status=status.HTTP_400_BAD_REQUEST,
)

# FIX VULN-02: reject comment creation when the issue does not belong to the
# board's project. Without this check a caller can attach a comment to any
# issue in the system β€” including issues from private projects β€” by supplying
# an arbitrary issue_id in the URL while using a public board as a proxy.
if not Issue.objects.filter(
pk=issue_id,
project_id=project_deploy_board.project_id,
workspace_id=project_deploy_board.workspace_id,
).exists():

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟑 Minor | ⚑ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify Issue model's manager definitions to confirm delta between `objects` and `issue_objects`.
ast-grep run --pattern 'class Issue($$$) {
  $$$
}' --lang python apps/api/plane/db/models
rg -n "issue_objects" apps/api/plane/db/models -A3 -B3

Repository: makeplane/plane

Length of output: 847


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Candidate files =="
git ls-files | rg '(^apps/api/plane/db/models/issue\.py$|^apps/api/plane/space/views/issue\.py$)'

echo
echo "== Issue model manager section =="
sed -n '1,230p' apps/api/plane/db/models/issue.py | nl -ba | sed -n '1,230p'

echo
echo "== views relevant sections =="
sed -n '250,285p' apps/api/plane/space/views/issue.py | nl -ba
sed -n '620,635p' apps/api/plane/space/views/issue.py | nl -ba

echo
echo "== Search for IssueManager/issue_objects uses =="
rg -n "class IssueManager|issue_objects = IssueManager|Issue\.issue_objects|Issue\.objects\.filter\(pk=issue_id|Issue.objects\.filter\(.*issue_id" apps/api -g '*.py'

Repository: makeplane/plane

Length of output: 320


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Issue model manager section =="
sed -n '1,230p' apps/api/plane/db/models/issue.py

echo
echo "== views relevant sections =="
sed -n '250,285p' apps/api/plane/space/views/issue.py
sed -n '620,635p' apps/api/plane/space/views/issue.py

echo
echo "== Search for IssueManager/issue_objects uses =="
rg -n "class IssueManager|issue_objects = IssueManager|Issue\.issue_objects|Issue\.objects\.filter\(pk=issue_id|Issue\.objects\.filter\(.*issue_id" apps/api -g '*.py'

Repository: makeplane/plane

Length of output: 32829


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== _issue_belongs_to_board helper definition =="
sed -n '1,120p' apps/api/plane/space/views/issue.py

echo
echo "== SoftDeletionManager definition =="
rg -n "class SoftDeletionManager|SoftDeletionManager" apps/api/plane/db apps/api/plane/utils -g '*.py' -A8 -B3

echo
echo "== ProjectBaseModel soft deletion fields =="
rg -n "class ProjectBaseModel|archived_at|deleted_at" apps/api/plane/db/models -g '*.py' -A10 -B5

Repository: makeplane/plane

Length of output: 50371


Use Issue.issue_objects for the board ownership check.

Issue.issue_objects is the soft-delete-aware manager and also excludes archived/draft issues, while _issue_belongs_to_board() currently uses plain Issue.objects. Use Issue.issue_objects here to reject comments on deleted or archived issues in the scope that is used for public issue listing and retrieval.

πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/plane/space/views/issue.py` around lines 273 - 277, Update the Issue
ownership query inside _issue_belongs_to_board() to use Issue.issue_objects
instead of Issue.objects, preserving the existing project_id and workspace_id
filters so deleted, archived, and draft issues are rejected.

return Response(
{"error": "Issue not found in this project."},
status=status.HTTP_404_NOT_FOUND,
)

serializer = IssueCommentSerializer(data=request.data)
if serializer.is_valid():
serializer.save(
Expand Down Expand Up @@ -345,16 +362,20 @@ class IssueReactionPublicViewSet(BaseViewSet):

def get_queryset(self):
try:
# FIX BONUS: the URL pattern is /anchor/<str:anchor>/issues/<uuid:issue_id>/reactions/
# which provides no "slug" or "project_id" kwargs. The old lookup via
# workspace__slug=self.kwargs.get("slug") and project_id=self.kwargs.get("project_id")
# always resolved both to None, causing DeployBoard.DoesNotExist on every list request
# and making reaction listing permanently broken on all public boards.
project_deploy_board = DeployBoard.objects.get(
workspace__slug=self.kwargs.get("slug"),
project_id=self.kwargs.get("project_id"),
anchor=self.kwargs.get("anchor"), entity_name="project"
)
if project_deploy_board.is_reactions_enabled:
return (
super()
.get_queryset()
.filter(workspace__slug=self.kwargs.get("slug"))
.filter(project_id=self.kwargs.get("project_id"))
.filter(workspace_id=project_deploy_board.workspace_id)
.filter(project_id=project_deploy_board.project_id)
.filter(issue_id=self.kwargs.get("issue_id"))
.order_by("-created_at")
.distinct()
Expand Down Expand Up @@ -525,8 +546,13 @@ class IssueVotePublicViewSet(BaseViewSet):

def get_queryset(self):
try:
# FIX VULN-04: the URL pattern is /anchor/<str:anchor>/issues/<uuid:issue_id>/votes/
# which provides no "slug" kwarg. The old lookup via
# workspace__slug=self.kwargs.get("anchor") passed an opaque anchor token as if it
# were a workspace slug, causing DeployBoard.DoesNotExist on every list request
# and making vote listing permanently broken on all public boards.
project_deploy_board = DeployBoard.objects.get(
workspace__slug=self.kwargs.get("anchor"), entity_name="project"
anchor=self.kwargs.get("anchor"), entity_name="project"
)
if project_deploy_board.is_votes_enabled:
return (
Expand Down Expand Up @@ -707,7 +733,7 @@ def get(self, request, anchor, issue_id):
id=F("issue_reactions__actor__id"),
first_name=F("issue_reactions__actor__first_name"),
last_name=F("issue_reactions__actor__last_name"),
avatar=F("issue_reactions__actor__avatar"),
avatar=F("issue_reactions__actor__actor__avatar"),
avatar_url=Case(
When(
votes__actor__avatar_asset__isnull=False,
Expand Down
Loading