GHSA-8g2p-pqm3-fcfh: praisonai-platform: Any workspace member can add arbitrary user as owner via POST /workspaces/{id}/members
Summary**Type:** Privilege escalation / cross-tenant member injection. The `POST /workspaces/{workspace_id}/members` endpoint is gated only by `require_workspace_member(workspace_id)` (default `min_role="member"`) and forwards the request body's `user_id` and `role` straight into `MemberService.add(workspace_id, user_id, role)`, which has no caller-permission check. A user with the lowest workspace privilege can add any user (including a new attacker-controlled second account, or an existing account they want to grief) as owner of the workspace.
**File:** `src/praisonai-platform/praisonai_platform/api/routes/workspaces.py`, lines 92-101; `services/member_service.py`, lines 26-38.
**Root cause:** `MemberService.add` validates only that `role` is in `VALID_ROLES = {"owner", "admin", "member"}` — the value, not the caller's right to assign it. The route's `Depends(require_workspace_member)` resolves to the default `min_role="member"`. So a member-level token plus one POST gives the attacker an alternate identity with owner role inside the same workspace, bypassing every owner-only operation that *would* otherwise gate them.Affected Code**File 1:** `src/praisonai-platform/praisonai_platform/api/routes/workspaces.py`, lines 92-101.**File 2:** `src/praisonai-platform/praisonai_platform/services/member_service.py`, lines 26-38.**Why it's wrong:** workspace member management is the textbook capability that must be gated on owner role. The role hierarchy is implemented (`MemberService.has_role`, member_service.py:80-96), the dependency-tunable `min_role` parameter exists (`require_workspace_member(min_role)`, deps.py:58), but the `POST .../members` route uses neither. The `VALID_ROLES` enum check is purely cosmetic — it accepts `"owner"` from any caller because the route never asked whether the caller has the right to assign that role.Exploit ChainAttacker registers two accounts (or recruits a member account on the target workspace `W`). Account A is an existing member of `W`; Account B is a fresh signup the attacker controls (any account on the platform — `auth/register` is open by default). State: attacker holds tokens for both A and B.Attacker authenticates as Account A and POSTs `Authorization: Bearer <A_jwt>` to `POST /workspaces/W/members` with body `{"user_id": "<B_user_id>", "role": "owner"}`. State: control flow enters `add_member`.`require_workspace_member(W, A)` passes (A is a member). `MemberService.add(W, B, "owner")` writes a new row `Member(workspace_id=W, user_id=B, role="owner")`. State: Account B is now a workspace-W owner.Attacker switches to Account B and acts as workspace owner — change settings, add/remove members, delete the workspace, or pivot to the companion advisories' primitives. State: attacker holds owner of any workspace they had member access to, via a fresh attacker-controlled identity that the original workspace's audit logs cannot easily attribute to A.Final state: with one member-level token plus one POST, the attacker plants an owner-role identity on any workspace they can reach. The same primitive lets the attacker invite a competitor or external-vendor account into the workspace as owner, exfiltrating the workspace's content under that competitor's name.Security Impact**Severity:** sec-critical. CVSS 9.1: network attack, low complexity, low privileges (member tier), no user interaction, scope changed (the new owner is a different security principal), high confidentiality and integrity, no availability claim.
**Attacker capability:** with one workspace-member token plus one POST request, the attacker grants owner-tier access to any user_id on the platform. From there, full workspace control via the Account B token, plus indirect attribution: the original workspace's audit logs see "user A added user B as owner" but the audit trail cannot tell that B is attacker-controlled.
**Preconditions:** `praisonai-platform` is deployed multi-tenant; the attacker has any membership token in the target workspace; the attacker can register or knows any other user_id on the platform.
**Differential:** source-inspection-verified. The asymmetry between `MemberService.has_role` (clearly tiered) and `add_member`'s default `min_role="member"` confirms the gap. With the suggested fix below, the gate refuses the member-tier token, the elevated POST returns 403, and the second-identity owner is never created.Suggested FixThe four other workspace mutation endpoints (`update_workspace`, `delete_workspace`, `update_member_role`, `remove_member`) exhibit the same default-min-role gap and are filed as their own advisories.
CVSS v3.1: 9.6