[WEB-8291] fix: gate project invitation list/retrieve/destroy to project admins (GHSA-r68c-48rr-m67f)#9444
Conversation
|
Linked to Plane Work Item(s) This comment was auto-generated by Plane |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (2)
📝 WalkthroughWalkthroughProject invitation list, retrieve, and delete endpoints now require project-admin permissions. Contract tests cover denial for non-members of the target project and successful listing by an active project administrator. ChangesProject invitation authorization
Estimated code review effort: 2 (Simple) | ~10 minutes Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Pull request overview
This PR hardens app API authorization around two previously vulnerable endpoints: project invitation list/retrieve/delete (GHSA-r68c-48rr-m67f / WEB-8291) and draft-to-issue conversion ownership scoping (GHSA-vfqm-7rh7-84hq / WEB-8289).
Changes:
- Restricts
ProjectInvitationsViewsetlist/retrieve/destroyactions to project admins via@allow_permission([ROLE.ADMIN]). - Scopes
WorkspaceDraftIssueViewSet.create_draft_to_issuedraft lookup tocreated_by=request.userand returns 404 when the draft isn’t owned by the caller. - Adds contract test coverage for both authorization/ownership scenarios.
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/project/invite.py | Gates invitations list/retrieve/destroy to project admins to prevent cross-project invite/token disclosure and deletion. |
| apps/api/plane/app/views/workspace/draft.py | Prevents non-owners from converting (and thereby consuming/migrating assets from) another user’s draft. |
| apps/api/plane/tests/contract/app/test_project_invite_list_scope_app.py | Contract regression tests ensuring non-members get 403 and project admins can still list invites. |
| apps/api/plane/tests/contract/app/test_draft_to_issue_owner_scope_app.py | Contract regression tests ensuring only the draft owner can convert; non-owners receive 404 and cannot steal assets. |
…ect admins (GHSA-r68c-48rr-m67f) ProjectInvitationsViewset declared no permission_classes (inheriting IsAuthenticated) and only decorated create with @allow_permission([ADMIN]). The default list/retrieve/destroy actions were ungated and get_queryset was scoped only by URL slug + project_id, so any authenticated user could read another project's pending invitations — including invitee email and the raw token (re-exposing what #9305 stripped from the public path) — and delete them. Gate list/retrieve/destroy with @allow_permission([ROLE.ADMIN]), matching create (project admin, or a workspace-admin who is a project member). The workspace sibling WorkspaceInvitationsViewset was already admin-gated. Also drop a pre-existing unused User import in the same file. Adds contract regression tests (fail-before verified): a non-member of the project is rejected with 403 on list/retrieve/destroy (invite left intact), with a positive control confirming a project admin can still list. Co-authored-by: Plane AI <noreply@plane.so>
82a315e to
e78665f
Compare
|
Good catch @copilot — that was a branching mistake, not intended scope. Fixed by rebasing
The draft-to-issue change stays tracked/reviewed independently in #9443. 4/4 invite contract tests still green; PR is mergeable. |
Confirmed — the branch now contains only the WEB-8291 project-invitation changes ( |
Summary
Fixes the confirmed-vulnerable half of GHSA-r68c-48rr-m67f (HIGH, CWE-862) — the project-invitation-list authorization bypass.
ProjectInvitationsViewsetdeclared nopermission_classes(inheritingIsAuthenticated) and only decoratedcreatewith@allow_permission([ROLE.ADMIN]). The defaultlist/retrieve/destroyactions were therefore ungated, andget_querysetwas scoped only by URLslug+project_id— not by the caller's membership. The serializer (ProjectMemberInviteSerializer,fields="__all__") returnsemailandtoken.Impact (before fix): any authenticated user could
GET /api/workspaces/<slug>/projects/<project_id>/invitations/(and/<pk>/) for a project they don't belong to and read every pending invite's rawtoken+ inviteeemail(re-exposing what #9305 stripped from the public path), and couldDELETEarbitrary project invitations.Fix
Gate
list/retrieve/destroywith@allow_permission([ROLE.ADMIN]), matchingcreate(project admin, or a workspace-admin who is a project member — the decorator's existing semantics). This mirrors the workspace siblingWorkspaceInvitationsViewset, which was already admin-gated (WorkSpaceAdminPermission).Chose the method decorator over a class-level
ProjectAdminPermissiondeliberately: a class-level permission would have tightenedcreate(blocking the workspace-admin-project-member fallback that@allow_permissionallows), a behavior change. The decorator keeps all four actions consistent.Also removes a pre-existing unused
Userimport in the same file.Tests
New contract suite
plane/tests/contract/app/test_project_invite_list_scope_app.py(fail-before verified — the 3 security tests return 200/204 without the fix):Verification
ruff check: cleanmanage.py check(test settings): no issuespytest(new suite): 4/4 green; fail-before verified🤖 Generated with Claude Code
Summary by CodeRabbit
Bug Fixes
Tests