Skip to content

[WEB-8352] fix(security): scope SubIssuesEndpoint to the URL project - #9466

Open
mguptahub wants to merge 3 commits into
previewfrom
web-8352/sub-issue-cross-project-scope
Open

[WEB-8352] fix(security): scope SubIssuesEndpoint to the URL project#9466
mguptahub wants to merge 3 commits into
previewfrom
web-8352/sub-issue-cross-project-scope

Conversation

@mguptahub

@mguptahub mguptahub commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

Summary

SubIssuesEndpoint (/workspaces/<slug>/projects/<project_id>/issues/<issue_id>/sub-issues/) is guarded only by ProjectEntityPermission, which verifies the caller belongs to the URL project_id but not that the path issue_id lives in that project. Both handlers then resolved issues without a project scope, so any member of one project could reach another project's issues in the same workspace.

Fixes WEB-8352 (CWE-639). Confirmed vulnerable against origin/preview @ a8e53b6ac7 by reading the canonical code.

Vulnerability

  • Read IDOR — GET filtered parent_id + workspace__slug only. With a cross-project issue_id, it returned that issue's sub-issues (names, priorities, assignees, labels, dates).
  • Write IDOR — POST loaded the parent by bare pk (no workspace/project scope) and filtered the moved sub-issues by workspace__slug only, letting a member re-parent issues from other projects — even other workspaces — under an arbitrary parent.

Fix

Scope every lookup to the URL project_id:

  • GET sub-issue queryset → + project_id.
  • POST parent lookup → + workspace__slug + project_id, returning 404 when the parent isn't in the caller's project (replaces the bare .get() that would otherwise 500).
  • POST sub_issue_ids filter and the response-builder queryset → + project_id.

Defense in depth: even with an in-project parent, sub_issue_ids from another project are now excluded.

Tests

apps/api/plane/tests/contract/app/test_sub_issue_cross_project_scope_app.py — 5 contract tests:

  • read cross-project sub-issues hidden
  • write cross-project re-parent blocked (404, parent untouched)
  • write cross-project sub_issue_ids ignored (in-project parent, foreign child untouched)
  • positive controls: same-project read + re-parent still work

Fail-before verified: with the fix reverted, the 3 security tests fail and the 2 positive controls pass; with the fix, all 5 pass. ruff + manage.py check clean.

EE

plane-ee vendors its own copy of the CE backend; an EE port will be assessed separately under Epic WEB-8293.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved access controls so sub-issue reads and re-parenting are limited to the correct project within a workspace.
    • Invalid parent issues now return a clear “not found” response.
    • Bulk sub-issue assignment ignores cross-project IDs and tracks only successfully re-parented items.
  • Tests

    • Added coverage for cross-project isolation and same-project re-parenting behavior.

…(GHSA-gxhv-fw9x-2pg3)

SubIssuesEndpoint is guarded only by ProjectEntityPermission, which verifies
the caller belongs to the URL project_id but not that the path issue_id lives
in that project. Both handlers then resolved issues without a project scope:

- GET filtered sub-issues by parent_id + workspace__slug only, leaking the
  names/priorities/assignees/dates of another project's sub-issues (read IDOR).
- POST loaded the parent by bare pk (no workspace/project scope) and filtered
  the moved sub-issues by workspace__slug only, letting any project member
  re-parent issues from other projects/workspaces (write IDOR).

Scope the parent lookup and both sub-issue querysets to the URL project_id
(and bind the parent to the workspace), returning 404 when the parent is not
in the caller's project. Adds 5 contract tests (3 security, 2 positive
controls); fail-before verified.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings July 23, 2026 08:59
@makeplane

makeplane Bot commented Jul 23, 2026

Copy link
Copy Markdown

@coderabbitai

coderabbitai Bot commented Jul 23, 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 Plus

Run ID: d6fdc7a4-0dcf-4ade-9e35-becdf904bc0e

📥 Commits

Reviewing files that changed from the base of the PR and between 42ca370 and 53ebf37.

📒 Files selected for processing (2)
  • apps/api/plane/app/views/issue/sub_issue.py
  • apps/api/plane/tests/contract/app/test_sub_issue_cross_project_scope_app.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/api/plane/app/views/issue/sub_issue.py
  • apps/api/plane/tests/contract/app/test_sub_issue_cross_project_scope_app.py

📝 Walkthrough

Walkthrough

The SubIssuesEndpoint now scopes GET and POST operations by workspace and project. Contract tests cover cross-project isolation, invalid parent handling, ignored foreign sub-issue IDs, activity suppression, and same-project behavior.

Changes

Sub-issue project scoping

Layer / File(s) Summary
Scope sub-issue endpoint operations
apps/api/plane/app/views/issue/sub_issue.py
GET and POST queries require matching workspace and project scope. Missing parents return 404 responses. Activity dispatch is limited to re-parented issues.
Validate project-scoped behavior
apps/api/plane/tests/contract/app/test_sub_issue_cross_project_scope_app.py
Contract fixtures and tests cover cross-project rejection, ignored foreign IDs, activity suppression, and same-project GET and POST behavior.

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

Possibly related PRs

Suggested reviewers: dheeru0198, pablohashescobar

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the security fix and the affected SubIssuesEndpoint scope.
Description check ✅ Passed The description explains the vulnerability, fix, tests, reference, and deployment scope in sufficient detail.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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-8352/sub-issue-cross-project-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 an IDOR (CWE-639) in SubIssuesEndpoint by ensuring all issue/sub-issue lookups are scoped to the URL project_id (and workspace), preventing cross-project access within the same workspace.

Changes:

  • Scope GET sub-issue queryset by project_id in addition to workspace__slug.
  • Scope POST parent issue lookup and sub_issue_ids updates/response queryset by workspace__slug + project_id, returning 404 when the parent is not in the URL project.
  • Add contract tests covering cross-project read/write attempts plus same-project positive controls.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
apps/api/plane/app/views/issue/sub_issue.py Adds project/workspace scoping to sub-issue read/write paths to prevent cross-project IDOR.
apps/api/plane/tests/contract/app/test_sub_issue_cross_project_scope_app.py Adds regression/contract coverage for cross-project sub-issue access and re-parenting scenarios.

Comment thread apps/api/plane/app/views/issue/sub_issue.py

@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: 2

🤖 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/app/views/issue/sub_issue.py`:
- Around line 230-242: Update the activity-dispatch loop in the sub-issue update
flow to iterate only the scoped `sub_issues` or `updated_sub_issues` queryset,
not raw `sub_issue_ids`. Preserve the existing activity payload while ensuring
events are emitted only for issues matching the workspace and project filters.

In `@apps/api/plane/tests/contract/app/test_sub_issue_cross_project_scope_app.py`:
- Around line 162-180: Extend test_write_cross_project_sub_issue_ids_ignored to
assert the successful response excludes orphan_b from both the returned
sub_issues collection and state_distribution, while retaining the existing
database-parent assertion.
🪄 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 Plus

Run ID: aab54f34-8991-43a5-a214-5a0cda32abc5

📥 Commits

Reviewing files that changed from the base of the PR and between a8e53b6 and 97767f9.

📒 Files selected for processing (2)
  • apps/api/plane/app/views/issue/sub_issue.py
  • apps/api/plane/tests/contract/app/test_sub_issue_cross_project_scope_app.py

Comment thread apps/api/plane/app/views/issue/sub_issue.py
…ssues (CodeRabbit/Copilot #9466)

The DB update + response were scoped to the URL project, but the activity loop
still iterated the raw caller-supplied sub_issue_ids. A cross-project id
(excluded from the re-parent) would still fire issue_activity.delay, whose task
does an unscoped Issue.objects.get and bumps updated_at — touching a foreign
issue and creating a bogus activity row.

Dispatch from the project-scoped sub_issues (scoped_sub_issue_ids) instead.
Strengthened the test to assert the foreign issue is absent from the response
body (sub_issues / state_distribution) and that no activity is dispatched for it
(mock). Fail-before verified.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 23, 2026 09:55

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/api/plane/app/views/issue/sub_issue.py (1)

253-260: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Record the actual parent ID in activity metadata.

Line 259 writes each child ID as parent, while requested_data correctly uses the URL parent (issue_id). Activity consumers will therefore see a self-parent relation rather than the applied re-parenting.

Proposed fix
-                current_instance=json.dumps({"parent": sub_issue_id}),
+                current_instance=json.dumps({"parent": str(issue_id)}),
🤖 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/app/views/issue/sub_issue.py` around lines 253 - 260, Update
the issue_activity.delay call in the sub-issue reparenting flow so
current_instance records the actual parent issue_id, matching requested_data,
instead of the child sub_issue_id. Keep the existing activity payload structure
and other arguments unchanged.
🤖 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.

Outside diff comments:
In `@apps/api/plane/app/views/issue/sub_issue.py`:
- Around line 253-260: Update the issue_activity.delay call in the sub-issue
reparenting flow so current_instance records the actual parent issue_id,
matching requested_data, instead of the child sub_issue_id. Keep the existing
activity payload structure and other arguments unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6f2ae175-1939-4677-81f9-96d17d55b82a

📥 Commits

Reviewing files that changed from the base of the PR and between 97767f9 and 42ca370.

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

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

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

@mguptahub mguptahub changed the title [WEB-8352] fix(security): scope SubIssuesEndpoint to the URL project (GHSA-gxhv-fw9x-2pg3) [WEB-8352] fix(security): scope SubIssuesEndpoint to the URL project Aug 7, 2026
Explanations kept unchanged; only the IDs are removed.

Co-authored-by: Plane AI <noreply@plane.so>
Copilot AI review requested due to automatic review settings August 7, 2026 10:34
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown

React Doctor found 4 issues in 4 files · 1 error & 3 warnings · score 82 / 100 (Needs work) · vs preview

Errors

3 warnings

core/components/issues/issue-layouts/spreadsheet/columns/label-column.tsx

  • ⚠️ L14 Import from a barrel file no-barrel-import

core/components/issues/peek-overview/properties.tsx

  • ⚠️ L42 Import from a barrel file no-barrel-import

core/components/issues/workspace-draft/draft-issue-properties.tsx

  • ⚠️ L31 Import from a barrel file no-barrel-import
⚠️ Warning: .github/workflows/react-doctor.yml is configured incorrectly. See below to fix.

React Doctor compares against preview to report only the issues this pull request introduces. This run couldn't complete that comparison (usually a shallow CI checkout with no merge base), so it listed every issue in the changed files, including ones that already existed on preview.

Add fetch-depth: 0 to the actions/checkout step in .github/workflows/react-doctor.yml so the checkout includes the history React Doctor needs:

 jobs:
   react-doctor:
     steps:
       - uses: actions/checkout@v5
+        with:
+          fetch-depth: 0

       - uses: millionco/react-doctor@v2

To silence this warning, set silence-missing-baseline-warning: true on the React Doctor action.

Reviewed by React Doctor for commit 53ebf37. See inline comments for fixes.

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

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

Suppressed comments (1)

apps/api/plane/app/views/issue/sub_issue.py:258

  • issue_activity uses current_instance to infer the previous parent (see track_parent() reading current_instance.get("parent")). Passing the sub-issue’s own id here ({"parent": sub_issue_id}) will make activity logs compute the old parent as the sub-issue itself, producing incorrect old/new identifiers (and potentially misleading audit history). Capture each sub-issue’s parent_id before mutating it and pass that as current_instance.parent.
                requested_data=json.dumps({"parent": str(issue_id)}),
                actor_id=str(request.user.id),
                issue_id=sub_issue_id,
                project_id=str(project_id),
                current_instance=json.dumps({"parent": sub_issue_id}),

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