PT-2026-45063 · Pypi · Praisonai-Platform
Published
2026-05-29
·
Updated
2026-05-29
·
CVE-2026-47409
CVSS v3.1
8.1
High
| Vector | AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:H |
Summary
Type: Authorization bypass enabling owner lockout. The
DELETE /workspaces/{workspace id}/members/{user id} endpoint is gated only by require workspace member(workspace id) (default min role="member"). Any member can remove any other member, including the workspace owner, using a single DELETE. There is no caller-role check, no target-role check, no "cannot remove last owner" guard.
File: src/praisonai-platform/praisonai platform/api/routes/workspaces.py, lines 130-140; services/member service.py, lines 71-78.
Root cause: MemberService.remove(workspace id, user id) performs the deletion without any caller-permission check or owner-protection logic. The route accepts the URL-supplied user id and dispatches it straight through. The role hierarchy (MemberService.has role) is implemented but never invoked here. A member-tier attacker can issue DELETE .../members/<owner user id> and immediately lock the legitimate owner out of the workspace.Affected Code
File 1:
src/praisonai-platform/praisonai platform/api/routes/workspaces.py, lines 130-140.@router.delete("/{workspace id}/members/{user id}", status code=status.HTTP 204 NO CONTENT)
async def remove member(
workspace id: str,
user id: str,
user: AuthIdentity = Depends(require workspace member), # <-- BUG: defaults to min role="member"
session: AsyncSession = Depends(get db),
):
member svc = MemberService(session)
removed = await member svc.remove(workspace id, user id) # <-- removes any member, including owner
if not removed:
raise HTTPException(status code=404, detail="Member not found")
File 2:
src/praisonai-platform/praisonai platform/services/member service.py, lines 71-78.async def remove(self, workspace id: str, user id: str) -> bool:
"""Remove a member from a workspace."""
member = await self.get(workspace id, user id)
if member is None:
return False
await self. session.delete(member) # <-- BUG: no caller-role check, no last-owner protection
await self. session.flush()
return True
Why it's wrong: member-removal is the textbook capability that must be gated on owner role. Removing the workspace owner is a permanent denial-of-service against the legitimate owner unless another owner exists. There must be (a) a caller min-role gate of "owner" or "admin", (b) a check that prevents removing a member whose role is higher than the caller's, and (c) a check that the workspace is left with at least one owner. None of these exist.
Exploit Chain
- Attacker is a member of workspace
Wwith role "member". State: attacker holds JWT. - Attacker enumerates the workspace owner's
user idviaGET /workspaces/W/members(list members has the same default-member gate, separate finding). Owner UUIDO idis now known. State: attacker holdsO id. - Attacker sends
DELETE /workspaces/W/members/O idwithAuthorization: Bearer <attacker jwt>. State: control flow entersremove member. require workspace member(W, attacker)passes (attacker is a member).MemberService.remove(W, O id)deletes the owner's member row. State:Member(workspace id=W, user id=O id, role="owner")is gone.- Owner attempts
GET /workspaces/W/...andrequire workspace member(W, O id)returns 403. State: legitimate owner is now locked out of their own workspace. - Combined with the
update member rolecompanion advisory, the attacker first promotes themselves to owner, then removes the legitimate owner, then has uncontested control. Combined withdelete workspace, the attacker wipes the workspace after kicking the owner. - Final state: with one member-level token, the attacker locks the legitimate owner out of their own workspace permanently. The owner has no recourse other than database-level admin intervention.
Security Impact
Severity: sec-high. CVSS 8.1: network attack, low complexity, low privileges, no user interaction, scope unchanged, no confidentiality, high integrity (membership table corrupted), high availability (legitimate owner cannot access their own workspace).
Attacker capability: with one workspace-member token plus one DELETE request, the attacker permanently locks any other member (including the workspace owner) out of the workspace.
Preconditions:
praisonai-platform is deployed multi-tenant; attacker has any membership token; owner's user id is reachable via the (unauthenticated-for-member) list members endpoint.
Differential: source-inspection-verified. The asymmetry between require workspace member's tunable min role parameter and this endpoint's use of the default value confirms the gap. With the suggested fix below, member-tier tokens fail the gate, and removing the workspace's last owner triggers the additional guard.Suggested Fix
--- a/src/praisonai-platform/praisonai platform/api/routes/workspaces.py
+++ b/src/praisonai-platform/praisonai platform/api/routes/workspaces.py
@@ -130,11 +130,21 @@
@router.delete("/{workspace id}/members/{user id}", status code=status.HTTP 204 NO CONTENT)
async def remove member(
workspace id: str,
user id: str,
- user: AuthIdentity = Depends(require workspace member),
+ user: AuthIdentity = Depends( require workspace owner),
session: AsyncSession = Depends(get db),
):
member svc = MemberService(session)
+ target = await member svc.get(workspace id, user id)
+ if target is not None and target.role == "owner":
+ # Refuse to remove the last owner.
+ owners = [m for m in await member svc.list members(workspace id) if m.role == "owner"]
+ if len(owners) <= 1:
+ raise HTTPException(status code=409, detail="Cannot remove the last workspace owner")
removed = await member svc.remove(workspace id, user id)
if not removed:
raise HTTPException(status code=404, detail="Member not found")
The four companion workspace-mutation endpoints exhibit the same default-min-role gap and are filed as their own advisories.
Fix
Missing Authorization
Improper Privilege Management
Found an issue in the description? Have something to add? Feel free to write us 👾
Related Identifiers
Affected Products
Praisonai-Platform