Skip to content

[WEB-8019] fix(security): scope CycleIssue reassignment lookup to workspace/project#9349

Open
mguptahub wants to merge 2 commits into
previewfrom
web-8019/cycle-issue-cross-tenant-bola
Open

[WEB-8019] fix(security): scope CycleIssue reassignment lookup to workspace/project#9349
mguptahub wants to merge 2 commits into
previewfrom
web-8019/cycle-issue-cross-tenant-bola

Conversation

@mguptahub

@mguptahub mguptahub commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fixes a cross-tenant write BOLA in CycleIssueViewSet.create (GHSA-4w5x-wc9w-f47x, medium).

POST /api/workspaces/<slug>/projects/<project_id>/cycles/<cycle_id>/cycle-issues/ looked up work items "already in another cycle" with:

CycleIssue.objects.filter(~Q(cycle_id=cycle_id), issue_id__in=issues)

This lookup was not scoped to the caller's workspace/project. An ADMIN/MEMBER of their own project could pass a work-item UUID belonging to a different workspace/project and have that foreign CycleIssue row reassigned to their own cycle — silently evicting the victim's work item from the victim's cycle and orphaning the row (project_id = victim, cycle_id = attacker).

The adjacent create path in the same handler was already scoped (with an explicit # prevent cross-tenant IDOR comment, added for GHSA-933r); the reassignment path above it was missed. Same class as GHSA-933r-rxg8-f3h2.

Fix

Scope the lookup to workspace__slug=slug + project_id=project_id, mirroring the adjacent create-path guard. Foreign-tenant rows are excluded from reassignment and are already dropped from the create path by the scoped new_issues query — so foreign issue IDs no longer produce any write.

Tests

tests/contract/app/test_cycle_issue_app.py:

  • Cross-tenant: attacker adds a foreign-tenant issue UUID to their cycle → the victim's CycleIssue stays in the victim's cycle, and no CycleIssue is created under the attacker's cycle. Fail-before verified via git stash (row was reassigned without the fix).
  • Same-tenant: a legitimate reassignment within the caller's own project still moves the issue to the target cycle (guard doesn't break the normal flow).

Verification

  • ruff check clean
  • python manage.py check clean
  • 2 passed

Summary by CodeRabbit

  • Bug Fixes
    • Improved sorting safety in issue and project lists so invalid or unsafe order_by values no longer cause errors.
    • Cross-cycle issue updates now stay within the correct workspace and project, preventing unexpected reassignment across tenants.
    • Added regression coverage to ensure valid sorting options still work as expected while unsafe inputs are handled gracefully.

mguptahub and others added 2 commits July 3, 2026 16:01
… endpoints

Close a partial bypass of WEB-7813 (GHSA-2r95 / GHSA-w45q): the external
REST API project-list and work-item-list endpoints passed a raw order_by
query parameter to Django's .order_by(). Because Django resolves
__-separated relational paths, an attacker could order by sensitive
columns on related tables (created_by__password / token / email) to build
a blind ordering oracle, or crash the endpoint (HTTP 500) with an unknown
field.

Route both endpoints through the existing sanitize_order_by() helper with
the appropriate allowlist (PROJECT_ORDER_BY_ALLOWLIST, default sort_order;
ISSUE_ORDER_BY_ALLOWLIST, default -created_at), mirroring how
order_issue_queryset() already sanitizes. Non-allowlisted values collapse
to the safe default; legitimate orderings are unchanged.

Adds unit tests (allowlist neutralisation + passthrough) and contract
tests asserting both endpoints return 200 (not 500) for injected fields;
fail-before verified via git stash.

Advisory: GHSA-p885-6jpg-cr2p

Co-authored-by: Plane AI <noreply@plane.so>
…kspace/project

CycleIssueViewSet.create looked up "issues already in another cycle" with
CycleIssue.objects.filter(~Q(cycle_id=cycle_id), issue_id__in=issues) —
without scoping to the caller's workspace/project. An ADMIN/MEMBER of their
own project could pass a work-item UUID from a different tenant and have that
foreign CycleIssue row reassigned to their cycle, silently evicting the
victim's work item from the victim's cycle (cross-tenant write / BOLA).

Scope the lookup to workspace__slug + project_id, mirroring the adjacent
create-path guard. Foreign-tenant rows are excluded from reassignment and
already dropped from the create path by the scoped new_issues query.

Adds a contract regression test proving a foreign-tenant CycleIssue row is
not reassigned (fail-before verified via git stash) plus a same-project
reassignment test to confirm the legitimate flow is unaffected.

Advisory: GHSA-4w5x-wc9w-f47x

Co-authored-by: Plane AI <noreply@plane.so>
@mguptahub mguptahub requested a review from dheeru0198 as a code owner July 3, 2026 10:59
Copilot AI review requested due to automatic review settings July 3, 2026 10:59
@makeplane

makeplane Bot commented Jul 3, 2026

Copy link
Copy Markdown

Linked to Plane Work Item(s)

This comment was auto-generated by Plane

@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds order_by sanitization via allowlists to issue and project list endpoints to prevent unsafe/injection-style ordering, and scopes the CycleIssue lookup query in CycleIssueViewSet.create by workspace and project to prevent cross-tenant matching. Contract and unit tests validate both fixes.

Changes

Order-by injection sanitization

Layer / File(s) Summary
Issue list order_by sanitization
apps/api/plane/api/views/issue.py
Imports sanitize_order_by and ISSUE_ORDER_BY_ALLOWLIST; ordering is computed via sanitize_order_by(...) with default "-created_at" instead of raw request parameter.
Project list order_by sanitization
apps/api/plane/api/views/project.py
Imports sanitize_order_by and PROJECT_ORDER_BY_ALLOWLIST; ordering is computed via sanitize_order_by(...) with default "sort_order" instead of raw request parameter.
Unit tests for sanitize_order_by
apps/api/plane/tests/unit/utils/test_order_by_sanitize.py
New tests verify injection payloads fall back to defaults and legitimate allowlisted values pass through unchanged for both endpoints.
Issue list contract tests
apps/api/plane/tests/contract/api/test_issues.py
New fixtures and tests confirm invalid, relational, and legitimate order_by values return HTTP 200 on the issue list endpoint.
Project list contract tests
apps/api/plane/tests/contract/api/test_projects.py
New tests confirm invalid and relational order_by values return HTTP 200 on the project list endpoint.

Cycle issue cross-tenant scoping fix

Layer / File(s) Summary
Scope CycleIssue lookup by workspace and project
apps/api/plane/app/views/cycle/issue.py
The existing CycleIssue query in create adds workspace__slug and project_id filters alongside the prior cycle/issue exclusion filter.
Cross-tenant BOLA regression tests
apps/api/plane/tests/contract/app/test_cycle_issue_app.py
New fixtures and tests set up attacker/victim tenants and verify foreign-tenant CycleIssue rows are not reassigned while same-tenant reassignment still works.

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

Possibly related PRs

  • makeplane/plane#9269: Both PRs modify CycleIssueViewSet.create to add workspace/project scoping when handling CycleIssue creation/selection.
  • makeplane/plane#9292: Both PRs route ordering through sanitize_order_by with endpoint-specific allowlists at the same issue/project listing code points.

Suggested reviewers: pablohashescobar, dheeru0198

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main security fix in the changeset.
Description check ✅ Passed The description covers the security fix, rationale, tests, and verification in a mostly complete way.
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-8019/cycle-issue-cross-tenant-bola

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

Hardens Plane’s REST API against two security issues by (1) preventing cross-tenant CycleIssue reassignment during cycle-issue creation and (2) sanitizing user-supplied order_by parameters for project and issue listing endpoints to block ORM relational-traversal ordering injection. Adds unit + contract regression tests to ensure both fixes remain enforced.

Changes:

  • Scope CycleIssueViewSet.create’s “already in another cycle” lookup to the caller’s workspace + project to prevent cross-tenant write/BOLA reassignment.
  • Sanitize order_by query params for project and issue list endpoints using sanitize_order_by() + endpoint-specific allowlists.
  • Add unit tests for sanitize_order_by() behavior and contract tests covering both the order-by injection regression and the cross-tenant cycle-issue reassignment regression.

Reviewed changes

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

Show a summary per file
File Description
apps/api/plane/app/views/cycle/issue.py Scopes reassignment lookup by workspace__slug + project_id to prevent cross-tenant CycleIssue updates.
apps/api/plane/api/views/project.py Sanitizes order_by for project listing using PROJECT_ORDER_BY_ALLOWLIST.
apps/api/plane/api/views/issue.py Sanitizes order_by for work-item listing using ISSUE_ORDER_BY_ALLOWLIST before any ordering logic runs.
apps/api/plane/tests/unit/utils/test_order_by_sanitize.py Adds unit-level regression coverage ensuring allowlists reject payloads and preserve legitimate fields (including descending variants).
apps/api/plane/tests/contract/api/test_projects.py Adds contract-level regression coverage ensuring malicious/invalid order_by no longer triggers HTTP 500 for projects list.
apps/api/plane/tests/contract/api/test_issues.py Adds contract-level regression coverage ensuring malicious/invalid order_by no longer triggers HTTP 500 for issues list.
apps/api/plane/tests/contract/app/test_cycle_issue_app.py Adds contract regression tests validating cross-tenant reassignment is blocked while same-tenant reassignment still works.

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

🧹 Nitpick comments (1)
apps/api/plane/tests/contract/app/test_cycle_issue_app.py (1)

139-176: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Same-tenant reassignment coverage looks good.

Confirms the scope guard doesn't break legitimate same-project reassignment.

One gap: only cross-workspace isolation is tested; consider adding a same-workspace-but-different-project case, since the fix also scopes by project_id and that boundary isn't currently exercised.

🤖 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/tests/contract/app/test_cycle_issue_app.py` around lines 139 -
176, Add a contract test for the project boundary in the cycle reassignment
flow: the current `test_same_tenant_reassignment_still_works` covers
same-project success, but you should also exercise the
`CycleIssue`/`CycleIssueView` path with an issue in the same workspace and a
different project to ensure the `project_id` scope guard blocks reassignment
across projects while still allowing the legitimate same-project case.
🤖 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.

Nitpick comments:
In `@apps/api/plane/tests/contract/app/test_cycle_issue_app.py`:
- Around line 139-176: Add a contract test for the project boundary in the cycle
reassignment flow: the current `test_same_tenant_reassignment_still_works`
covers same-project success, but you should also exercise the
`CycleIssue`/`CycleIssueView` path with an issue in the same workspace and a
different project to ensure the `project_id` scope guard blocks reassignment
across projects while still allowing the legitimate same-project case.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 77919783-a866-4ed8-ac23-a7175fdfe7d9

📥 Commits

Reviewing files that changed from the base of the PR and between 7fbf14a and a33795c.

📒 Files selected for processing (7)
  • apps/api/plane/api/views/issue.py
  • apps/api/plane/api/views/project.py
  • apps/api/plane/app/views/cycle/issue.py
  • apps/api/plane/tests/contract/api/test_issues.py
  • apps/api/plane/tests/contract/api/test_projects.py
  • apps/api/plane/tests/contract/app/test_cycle_issue_app.py
  • apps/api/plane/tests/unit/utils/test_order_by_sanitize.py

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