Skip to content

[WEB-8333] fix(security): guard undecorated viewset routes with project-membership check - #9461

Open
mguptahub wants to merge 5 commits into
previewfrom
web-8333/undecorated-route-idor
Open

[WEB-8333] fix(security): guard undecorated viewset routes with project-membership check#9461
mguptahub wants to merge 5 commits into
previewfrom
web-8333/undecorated-route-idor

Conversation

@mguptahub

@mguptahub mguptahub commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

Summary

HIGH — fixes a cross-project/tenant IDOR (WEB-8333). Confirmed-vulnerable on origin/preview.

Per-project authorization in these viewsets lives in the @allow_permission(..., level="PROJECT") method decorator, not the queryset (get_queryset filters only workspace__slug+project_id, no membership predicate). The project-wide default permission is IsAuthenticated only, and there's no has_object_permission anywhere — so any routed action without the decorator falls through to stock DRF ModelViewSet and any authenticated user (even a non-member of the project) can act cross-project.

Undecorated fall-through routes:

  • IssueViewSet.update (PUT) — no def update
  • ModuleViewSet.update (PUT) — no def update
  • IntakeViewSet.retrieve (GET) + IntakeViewSet.partial_update (PATCH) — only list/create/destroy were defined

Fix

Define + decorate the missing handlers, matching each viewset's sibling role set:

Viewset Added Decorator (matches sibling)
IssueViewSet update @allow_permission([ADMIN, MEMBER], creator=True, model=Issue)
ModuleViewSet update @allow_permission([ADMIN, MEMBER])
IntakeViewSet retrieve, partial_update @allow_permission([ADMIN, MEMBER])

PUT is routed through the authorized partial_update path for Issue/Module — no full-replace semantics exist for these resources (the FE only PATCHes), so this is purely additive authorization, not a behavior change reviewers need to worry about. If a strict full-PUT-replace is ever desired that's a separate behavioral change. Intake retrieve/partial_update delegate to stock DRF (behavior preserved). Both intakes/ and legacy inboxes/ routes map to the same viewset, so both are covered.

Tests

New tests/contract/app/test_undecorated_route_project_scope_app.py — 8 cases (non-member cross-project PUT issue / PUT module / GET intake / PATCH intake → 403; + 4 positive controls). Fail-before verified on the CE docker stack: 4 attack routes returned 200 unpatched → all 8 pass patched. (Full contract/app regression: 109 pass; the 8 failures are pre-existing rate-limit flakes in test_authentication.py, unrelated.)

Summary by CodeRabbit

  • Bug Fixes

    • Strengthened authorization for project-scoped issue, module, and intake endpoints, ensuring only admin and project members can update/retrieve.
    • Prevented unauthorized users from accessing previously misrouted update/retrieve actions, returning 403 Forbidden instead.
  • Tests

    • Added contract/regression coverage for PUT/PATCH/GET flows across issue, module, and intake, including intake route aliasing (intakes/inboxes).
    • Verified 403 for non-project members and expected 200/204 plus correct field behavior for authorized project members.

…ct-membership check (GHSA-27v6 / GHSA-w83f)

Per-project authz lives in the @allow_permission(level="PROJECT") method
decorator, not the queryset. IssueViewSet/ModuleViewSet had no `def update`
(PUT), and IntakeViewSet had no `retrieve`/`partial_update` — those routes fell
through to stock DRF ModelViewSet under the project-wide default IsAuthenticated
(no has_object_permission anywhere), letting any authenticated non-member act
cross-project (read/modify issues, modules, intakes in projects they don't belong to).

Define + decorate the missing handlers, matching each viewset's sibling role set:
- IssueViewSet.update  → @allow_permission([ADMIN, MEMBER], creator=True, model=Issue), routes PUT through the authorized partial_update path
- ModuleViewSet.update → @allow_permission([ADMIN, MEMBER]), routes PUT through partial_update
- IntakeViewSet.retrieve + partial_update → @allow_permission([ADMIN, MEMBER]), delegate to stock DRF

PUT is routed through PATCH because no full-replace semantics exist for these
resources (the FE only ever PATCHes); the fix is purely additive authorization.

Adds 8 contract tests; fail-before verified (4 attack routes returned 200 unpatched → all 8 pass patched).

Co-authored-by: Plane AI <noreply@plane.so>
Copilot AI lite review requested due to automatic review settings July 22, 2026 12:10
@makeplane

makeplane Bot commented Jul 22, 2026

Copy link
Copy Markdown

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@mguptahub, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 46 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 50dfea33-7564-49f6-b742-26e9664566c7

📥 Commits

Reviewing files that changed from the base of the PR and between 3b7f517 and 96a5115.

📒 Files selected for processing (1)
  • apps/api/plane/tests/contract/app/test_undecorated_route_project_scope_app.py
📝 Walkthrough

Walkthrough

The change adds explicit role-guarded handlers for issue, module, and intake routes, routing updates through existing partial-update logic. Contract tests verify project members succeed while non-members receive forbidden responses across current and legacy intake routes.

Changes

Project-scope authorization

Layer / File(s) Summary
Guarded route handlers
apps/api/plane/app/views/{issue,module,intake}/base.py
Issue and module PUT handlers delegate to guarded partial updates; intake retrieve and partial-update handlers apply explicit admin/member permission checks.
Project-scope contract coverage
apps/api/plane/tests/contract/app/test_undecorated_route_project_scope_app.py
Fixtures and tests verify forbidden non-member access and successful member operations for issue, module, and intake routes, including the intakes and legacy inboxes aliases.

Estimated code review effort: 3 (Moderate) | ~20 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 38.10% 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
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.
Title check ✅ Passed The title clearly identifies the security fix and the project-membership authorization scope.
Description check ✅ Passed The description clearly explains the security issue, implementation, tests, and WEB-8333 reference, although it does not use all template headings.
✨ 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-8333/undecorated-route-idor

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.

@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

🤖 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/tests/contract/app/test_undecorated_route_project_scope_app.py`:
- Around line 55-56: The contract tests currently cover only the intakes route
and omit its legacy inboxes alias. Update _intake_detail_url and the related
GET/PATCH member and non-member test cases to parameterize or otherwise exercise
both /intakes/ and /inboxes/ while preserving the existing assertions for each
route.
🪄 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: 4074818a-876d-470a-9404-621bdf231418

📥 Commits

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

📒 Files selected for processing (4)
  • apps/api/plane/app/views/intake/base.py
  • apps/api/plane/app/views/issue/base.py
  • apps/api/plane/app/views/module/base.py
  • apps/api/plane/tests/contract/app/test_undecorated_route_project_scope_app.py

Comment thread apps/api/plane/tests/contract/app/test_undecorated_route_project_scope_app.py Outdated

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 fixes a high-severity cross-project/tenant IDOR by ensuring routed DRF actions that previously fell through to ModelViewSet defaults (and thus only IsAuthenticated) are now explicitly implemented and guarded with the existing project-scoped @allow_permission(...) checks.

Changes:

  • Add update() handlers for Issue/Module viewsets and route PUT through the already-authorized partial_update() path.
  • Add explicit retrieve() and partial_update() handlers to IntakeViewSet, delegating to DRF’s stock implementations but now protected by @allow_permission.
  • Add a contract test suite covering the previously-undecorated routes (403 for cross-project non-members + positive controls).

Reviewed changes

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

File Description
apps/api/plane/app/views/issue/base.py Adds a guarded update() implementation to prevent undecorated PUT falling through without project authorization.
apps/api/plane/app/views/module/base.py Adds a guarded update() implementation to prevent undecorated PUT falling through without project authorization.
apps/api/plane/app/views/intake/base.py Adds guarded retrieve() and partial_update() implementations so routed actions no longer fall through without project authorization.
apps/api/plane/tests/contract/app/test_undecorated_route_project_scope_app.py Adds regression/contract coverage for the previously-fallthrough routes.

…lot #9461)

The positive-control tests authenticate as create_user, who was an ADMIN (role
20) member of project_b — so they exercised the ADMIN path, not MEMBER, despite
the "member" docstrings. Make create_user a project MEMBER (role 15) so the
positive controls validate the ROLE.MEMBER branch of @allow_permission on the
newly-guarded handlers (all allow [ADMIN, MEMBER]; admin is covered elsewhere).

Co-authored-by: Plane AI <noreply@plane.so>
Copilot AI review requested due to automatic review settings July 22, 2026 12:36

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 4 out of 4 changed files in this pull request and generated 1 comment.

…ssue PUT (CodeRabbit/Copilot #9461)

Address two review comments:

- CodeRabbit: parameterize the IntakeViewSet retrieve/partial_update contract
  tests over both the `intakes/` route and the legacy `inboxes/` alias, so the
  alias (which routes to the same viewset) cannot regress independently.
- Copilot: issue_b is now authored by a distinct project-B member (via
  save(created_by_id=...)) instead of create_user, so test_member_can_put_issue
  is authorized through the ROLE.MEMBER branch of allow_permission rather than
  the creator=True short-circuit. (BaseModel.save silently nulls created_by
  under tests, so the previous created_by=create_user kwarg was a no-op.)

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:20

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 4 out of 4 changed files in this pull request and generated 2 comments.

Comment thread apps/api/plane/app/views/issue/base.py Outdated
Comment thread apps/api/plane/app/views/module/base.py Outdated
…ilot #9461)

IssueViewSet.update and ModuleViewSet.update are decorated with allow_permission
and delegated to the *decorated* partial_update, so allow_permission (and its
ProjectMember/WorkspaceMember lookups) ran twice on every PUT.

Delegate to partial_update.__wrapped__ (the undecorated implementation) instead:
update's own decorator has already authorized the request, so the check runs
exactly once while the routed action stays visibly guarded.

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:31

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 4 out of 4 changed files in this pull request and generated no new comments.

@mguptahub mguptahub changed the title [WEB-8333] fix(security): guard undecorated viewset routes with project-membership check (GHSA-27v6 / GHSA-w83f) [WEB-8333] fix(security): guard undecorated viewset routes with project-membership check Aug 7, 2026
Explanations kept unchanged; only the IDs are removed.

Co-authored-by: Plane AI <noreply@plane.so>
@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 96a5115. 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 4 out of 4 changed files in this pull request and generated no new comments.

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