* [WEB-8283] fix: bind Spaces board object IDs to the anchor's project
The public Spaces board endpoints resolved the DeployBoard from the URL
anchor but trusted the caller-supplied issue_id/comment_id/intake_id
verbatim, without verifying the object belonged to that board's
project/workspace. Any authenticated user could write comments,
reactions and votes onto arbitrary issues cross-tenant, and read EXTERNAL
comments from a different project in the same workspace.
Bind every caller-supplied object id to the board's project + workspace
before writing:
- comment / issue-reaction / vote create: require the issue to exist in
the board's project via Issue.issue_objects (excludes draft/archived/
triage), else 404.
- comment-reaction create: require the comment to exist in the board's
project with access="EXTERNAL", else 404.
- intake create: require the URL intake_id to match the board's intake,
else 400.
- comment list read: scope the queryset to the board's project_id.
Also add the missing is_votes_enabled gate on vote create for parity with
comment/reaction create (pre-existing gap in the same method).
Adds contract regression tests (fail-before verified): cross-tenant
writes and the cross-project comment read now rejected, with positive
controls confirming legitimate board writes/reads still succeed.
Co-authored-by: Plane AI <noreply@plane.so>
* [WEB-8283] test: address Copilot review — cross-workspace + votes-disabled coverage
- Add cross-workspace write test (issue in a different workspace) to
exercise the workspace_id binding, matching the advisory's cross-tenant
impact (previously only same-workspace/different-project was covered).
- Add a regression test for the new is_votes_enabled gate on vote create
(votes-disabled board → 400), preventing the pre-existing gap from
reappearing.
- Clarify the section header comment: cross-tenant writes return 404 for
issue/comment binding, 400 for the intake binding mismatch.
Co-authored-by: Plane AI <noreply@plane.so>
* [WEB-8283] refactor: extract board-scope guards into shared helpers
Address CodeRabbit review: the identical "object belongs to the board's
project+workspace" existence check was duplicated across four create()
methods (in four different ViewSets). Extract two module-level helpers —
_issue_in_board_scope and _comment_in_board_scope — so the check is a
single source of truth and cannot drift between endpoints or be forgotten
on a new one (the exact class of bug this PR fixes).
Behavior-preserving; 14 contract tests still green.
Co-authored-by: Plane AI <noreply@plane.so>
* chore(security): drop advisory identifiers from code comments
Explanations kept unchanged; only the IDs are removed.
Co-authored-by: Plane AI <noreply@plane.so>
* fix(security): use the deploy board's project_id in the reaction-create activity log
CommentReactionPublicViewSet.create() logged the activity with
str(self.kwargs.get("project_id", None)) — this route's URL only ever
supplies anchor and comment_id, never project_id, so every comment
reaction created on a public board logged project_id="None", silently
corrupting the activity/audit trail. destroy() on the same viewset
already resolves the correct project_id from the deploy board; create()
now does the same.
Co-authored-by: Plane AI <noreply@plane.so>
* [WEB-8283] fix: apply board-scope guards to comment/reaction read, update and delete paths
The board/issue/external-comment scoping added by this PR's create() methods
was never applied to the list, update and delete paths built on the same
models. A caller could read reactions on an INTERNAL (non-public) comment
through the public reaction list, or reach a comment or reaction they
authored through a board it doesn't actually belong to via partial_update()
or destroy(), since those methods looked up objects by pk/actor only.
Bind IssueCommentPublicViewSet.partial_update()/destroy() to the board's
project, workspace, issue_id and EXTERNAL access; bind
IssueReactionPublicViewSet.destroy() to the board's project (previously only
workspace-scoped); and bind CommentReactionPublicViewSet.get_queryset()/
destroy() to EXTERNAL comments only. Add regression coverage for each gap,
plus positive controls confirming legitimate reads/writes on the board's own
objects still work.
Co-authored-by: Plane AI <noreply@plane.so>
---------
Co-authored-by: Plane AI <noreply@plane.so>