Skip to content

[WEB-8283] fix: bind Spaces board object IDs to the anchor's project (GHSA-vqr2-wx56-gmq4)#9442

Open
mguptahub wants to merge 3 commits into
previewfrom
web-8283/spaces-board-object-id-scope
Open

[WEB-8283] fix: bind Spaces board object IDs to the anchor's project (GHSA-vqr2-wx56-gmq4)#9442
mguptahub wants to merge 3 commits into
previewfrom
web-8283/spaces-board-object-id-scope

Conversation

@mguptahub

@mguptahub mguptahub commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fixes GHSA-vqr2-wx56-gmq4 (HIGH, CWE-862). The public Spaces board endpoints (/api/public/anchor/<anchor>/...) resolved the DeployBoard from the URL anchor but then trusted the caller-supplied issue_id / comment_id / intake_id verbatim, without verifying the target object belonged to that board's project/workspace.

Impact (before fix): any authenticated user could

  • write comments, reactions and votes onto arbitrary issues in any project/workspace (cross-tenant write), and
  • read EXTERNAL comments from a different project in the same workspace (cross-project read).

Fix

Bind every caller-supplied object id to the board's project + workspace before writing (apps/api/plane/space/views/issue.py, intake.py):

Endpoint Guard
comment create issue must exist in board's project (Issue.issue_objects) → else 404
issue-reaction create same → else 404
vote create same → else 404
comment-reaction create comment must exist in board's project with access="EXTERNAL" → else 404
intake create URL intake_id must equal board.intake_id → else 400
comment list (read) queryset scoped to board.project_id

Issue.issue_objects is used deliberately — it excludes draft/archived/triage issues, matching exactly what the board already displays, so no legitimate write is rejected.

Also adds the missing is_votes_enabled gate on vote create for parity with comment/reaction create (a pre-existing gap in the same method).

Tests

New contract suite plane/tests/contract/app/test_spaces_board_object_scope_app.py (12 tests, fail-before verified — the 6 security tests fail without the fix):

  • Cross-tenant write rejected for comment / issue-reaction / vote / comment-reaction; foreign intake rejected.
  • Cross-project comment read no longer leaks.
  • Positive controls confirm legitimate board comment / reaction / comment-reaction / vote / intake writes and the board's own comment read still succeed.

Verification

  • ruff check: clean
  • manage.py check (test settings): no issues
  • pytest (new suite + sibling deploy-board suite): all green

Notes / out of scope

  • This is the same class as GHSA-x8wp-w37j-qq57, previously closed as a duplicate but whose write vector was never actually remediated — this PR covers it.
  • Two pre-existing, low-severity items left for separate follow-up: publish-side intake→board binding is unvalidated (admin-gated, not reachable via the public anchor), and asset.py comment_id is not bound (non-cross-tenant integrity nit).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Strengthened public board write validation to ensure issues, comments, reactions, votes, and intake issues are strictly scoped to the board’s project/workspace.
    • Tightened comment listing to avoid leaking out-of-scope project content.
    • Added clearer 400/404 responses when targets are invalid or not found in the board scope.
  • Tests

    • Expanded contract coverage for cross-project/workspace rejection, comment access behavior, and vote creation respecting the “votes enabled” feature gate (including when votes are disabled).

…(GHSA-vqr2-wx56-gmq4)

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>
@mguptahub
mguptahub requested a review from dheeru0198 as a code owner July 20, 2026 04:31
Copilot AI review requested due to automatic review settings July 20, 2026 04:31
@makeplane

makeplane Bot commented Jul 20, 2026

Copy link
Copy Markdown

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 43afef46-a53c-4db5-a925-d063f2303b80

📥 Commits

Reviewing files that changed from the base of the PR and between 24faac6 and 06de0d6.

📒 Files selected for processing (1)
  • apps/api/plane/space/views/issue.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/api/plane/space/views/issue.py

📝 Walkthrough

Walkthrough

Public Spaces board endpoints now scope caller-supplied issue, comment, reaction, vote, and intake identifiers to the deploy board’s project and workspace. Contract tests cover rejected cross-project access, read isolation, feature gating, and valid board-owned operations.

Changes

Spaces board object scoping

Layer / File(s) Summary
Issue and comment object validation
apps/api/plane/space/views/issue.py
Public comment, reaction, comment-reaction, and vote operations validate target ownership against the deploy board scope; comment listing also filters by project.
Intake-to-board binding
apps/api/plane/space/views/intake.py
Intake issue creation rejects intake IDs that do not match the deploy board’s intake.
Object-scope contract coverage
apps/api/plane/tests/contract/app/test_spaces_board_object_scope_app.py
Tests cover cross-project and cross-workspace rejection, comment read isolation, vote feature gating, and successful operations using board-owned objects.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Possibly related PRs

Suggested reviewers: dheeru0198

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 32.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main change: binding Spaces board object IDs to the board's project scope.
Description check ✅ Passed The description is detailed and covers the fix, tests, verification, and references, though it doesn't use the exact template headings or checkboxes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch web-8283/spaces-board-object-id-scope

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

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.

Pull request overview

This PR addresses GHSA-vqr2-wx56-gmq4 (CWE-862) by binding caller-supplied object IDs in public Spaces board endpoints (/api/public/anchor/<anchor>/...) to the resolved DeployBoard’s project + workspace, preventing cross-project/workspace writes (comments/reactions/votes/intake) and cross-project EXTERNAL comment reads.

Changes:

  • Scope public comment-list queries to the board’s project_id in addition to workspace_id.
  • Add project/workspace binding checks for issue_id and comment_id on public write endpoints (comments, issue reactions, comment reactions, votes).
  • Bind intake_id to the board’s configured intake and add a new contract test suite covering the regression.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
apps/api/plane/tests/contract/app/test_spaces_board_object_scope_app.py Adds contract coverage for public anchor endpoints to prevent cross-project object-ID abuse.
apps/api/plane/space/views/issue.py Scopes comment list by board project and validates issue/comment IDs belong to the board before writes; adds missing votes-enabled gate.
apps/api/plane/space/views/intake.py Validates the URL intake_id matches the board’s intake before creating intake issues.

Comment thread apps/api/plane/space/views/issue.py
Comment thread apps/api/plane/space/views/issue.py Outdated

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
apps/api/plane/space/views/issue.py (1)

267-275: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the repeated board-scope existence check into a shared helper.

The same "does this object belong to the board's project+workspace" check (existence query + 404 on mismatch) is duplicated near-verbatim across four create() methods. Centralizing it reduces the risk that a future endpoint forgets the check or that one copy drifts from the others (this is exactly the class of bug this PR is fixing).

♻️ Proposed helper extraction
+    `@staticmethod`
+    def _issue_in_board_scope(issue_id, project_deploy_board):
+        return Issue.issue_objects.filter(
+            id=issue_id,
+            project_id=project_deploy_board.project_id,
+            workspace_id=project_deploy_board.workspace_id,
+        ).exists()
+
     def create(self, request, anchor, issue_id):
         ...
-        if not Issue.issue_objects.filter(
-            id=issue_id,
-            project_id=project_deploy_board.project_id,
-            workspace_id=project_deploy_board.workspace_id,
-        ).exists():
+        if not self._issue_in_board_scope(issue_id, project_deploy_board):
             return Response({"error": "Issue not found"}, status=status.HTTP_404_NOT_FOUND)

Apply similarly (with a CommentReaction/IssueComment variant) to the comment-reaction check at Line 481-487.

Also applies to: 385-393, 479-488, 574-589

🤖 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 267 - 275, Extract the
repeated board-scope existence query and 404 response into a shared helper used
by the four create() methods, including the Issue and
CommentReaction/IssueComment variants. Update the checks near the issue and
comment-reaction paths (and the corresponding locations) to call the helper with
the object identifier and board scope, preserving the existing “Issue not found”
or appropriate object-not-found response and preventing writes when project_id
or workspace_id do not match.
🤖 Prompt for all review comments with 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.

Inline comments:
In `@apps/api/plane/space/views/issue.py`:
- Around line 267-275: Update the issue ownership checks around the visible
guard and the corresponding checks near the referenced comment/reaction/vote
handlers to use the unfiltered Issue.objects manager instead of
Issue.issue_objects, so archived issues remain eligible when they belong to the
board’s project and workspace.

---

Nitpick comments:
In `@apps/api/plane/space/views/issue.py`:
- Around line 267-275: Extract the repeated board-scope existence query and 404
response into a shared helper used by the four create() methods, including the
Issue and CommentReaction/IssueComment variants. Update the checks near the
issue and comment-reaction paths (and the corresponding locations) to call the
helper with the object identifier and board scope, preserving the existing
“Issue not found” or appropriate object-not-found response and preventing writes
when project_id or workspace_id do not match.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 52d19334-1a5f-418c-bd40-b56b47ebf9cd

📥 Commits

Reviewing files that changed from the base of the PR and between 7cef741 and 6a7fd87.

📒 Files selected for processing (3)
  • apps/api/plane/space/views/intake.py
  • apps/api/plane/space/views/issue.py
  • apps/api/plane/tests/contract/app/test_spaces_board_object_scope_app.py

Comment thread apps/api/plane/space/views/issue.py Outdated
…abled 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>
@mguptahub

Copy link
Copy Markdown
Collaborator Author

Thanks @copilot — all three test-coverage points addressed in 24faac6 (production logic unchanged):

  1. Misleading section header (404 vs intake 400) — reworded the header to state that cross-tenant writes return 404 for the issue/comment binding and 400 for the intake binding mismatch.
  2. is_votes_enabled gate untested — added test_cannot_vote_when_votes_disabled (a board with voting disabled → 400), locking in the parity fix so the original gap can't reappear.
  3. Only same-workspace victim covered — added test_cannot_comment_on_issue_in_other_workspace (issue in a different workspace) to exercise the workspace_id binding directly, matching the advisory's cross-tenant impact.

Suite is now 14 tests (7 negative + 7 positive controls), all green; fail-before still verified for the security cases.

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>
@mguptahub

Copy link
Copy Markdown
Collaborator Author

Thanks @coderabbitai — addressed in 06de0d6:

✅ Helper extraction (nitpick). Pulled the duplicated board-scope check into two module-level helpers, _issue_in_board_scope / _comment_in_board_scope, now used by all four create() methods (they live in four different ViewSets, so module-level rather than a staticmethod). Single source of truth, no drift risk. Behavior-preserving — 14 contract tests still green.

↩️ Issue.issue_objects vs archived issues (potential issue) — intentional, keeping as-is. The public board only ever displays issues through issue_objects (both ProjectIssuesPublicEndpoint and IssueRetrievePublicEndpoint use it — archived/draft/triage are never rendered on the board). So the write guard using the same manager is consistent: a visitor can only comment/react/vote on what the board actually shows, and no board-visible issue is wrongly 404'd. Switching to Issue.objects would widen the write surface to archived/draft/triage issues the board deliberately hides, which is undesirable for this public surface. Documented the rationale in the helper docstring.

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@mguptahub Thanks for the detailed follow-up. The module-level scope helpers appropriately centralize the shared authorization boundary across the four viewsets.

The Issue.issue_objects choice also makes sense: matching the public board’s displayed-issue scope prevents writes to archived, draft, and triage issues that are intentionally not exposed through the board. The documented rationale and expanded regression coverage address the concerns.

🐇 ✅


✏️ Learnings added
Learnt from: mguptahub
URL: https://github.com/makeplane/plane/pull/9442

Timestamp: 2026-07-20T04:49:02.916Z
Learning: In `apps/api/plane/space/views/issue.py`, public Spaces board endpoints should use `Issue.issue_objects` for issue scope checks. Public board display endpoints (`ProjectIssuesPublicEndpoint` and `IssueRetrievePublicEndpoint`) use the same manager and intentionally exclude archived, draft, and triage issues; using `Issue.objects` for public comment, reaction, or vote authorization would widen the public write surface beyond board-visible issues.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants