GHSA-c2m8-4gcg-v22g CVE-2026-47416 PYSEC-2026-481
praisonai-platform: Any workspace member can promote themselves or others to owner via PATCH /workspaces/{id}/members/{user_id}
- Severity
- 9.6 / 10
- Critical · CVSS 3.1
- Exploit chance (EPSS)
- 0.26%
- Chance of exploitation in the next 30 days. Higher than 18.34% of scored vulnerabilities.
- Known exploited
- No
- Not in CISA's catalog
- Fix
0.1.4- Upgrade praisonai-platform to this version or later
Description
Summary
Type: Vertical privilege escalation. The PATCH /workspaces/{workspace_id}/members/{user_id} endpoint is gated by require_workspace_member(workspace_id), which defaults to min_role="member" and is never overridden by the route. The handler then calls MemberService.update_role(workspace_id, user_id, body.role) which sets the target member's role to whatever the request body specifies, with no check that the caller has owner-or-admin privilege, no check that the new role is not higher than the caller's own, and no check that the caller is not silently promoting themselves.
File: src/praisonai-platform/praisonai_platform/api/routes/workspaces.py, lines 115-127; services/member_service.py, lines 55-69; api/deps.py, lines 54-73.
Root cause: require_workspace_member exists with a min_role parameter (deps.py:58) but FastAPI's Depends(require_workspace_member) cannot pass arguments, so every route uses the default "member". The route then passes the URL-supplied user_id and the body-supplied role directly to MemberService.update_role, which contains zero permission checks: it loads the member by composite key and assigns member.role = new_role. A user with the lowest possible privilege ("member") thus sets their own role to "owner" with one HTTP PATCH, completing a member-to-owner privilege escalation in a single request.
Affected Code
File 1: src/praisonai-platform/praisonai_platform/api/routes/workspaces.py, lines 115-127.
@router.patch("/{workspace_id}/members/{user_id}", response_model=MemberResponse)
async def update_member_role(
workspace_id: str,
user_id: str,
body: MemberUpdate,
user: AuthIdentity = Depends(require_workspace_member), # <-- BUG: defaults to min_role="member"; no role gate
session: AsyncSession = Depends(get_db),
):
member_svc = MemberService(session)
member = await member_svc.update_role(workspace_id, user_id, body.role) # <-- writes any role to any member
if member is None:
raise HTTPException(status_code=404, detail="Member not found")
return MemberResponse.model_validate(member)
File 2: src/praisonai-platform/praisonai_platform/services/member_service.py, lines 55-69.
async def update_role(
self,
workspace_id: str,
user_id: str,
new_role: str,
) -> Optional[Member]:
"""Update a member's role."""
if new_role not in VALID_ROLES: # only validates the *value*, not the *caller's right*
raise ValueError(f"Invalid role: {new_role}. Must be one of {VALID_ROLES}")
member = await self.get(workspace_id, user_id)
if member is None:
return None
member.role = new_role # <-- BUG: no caller-role check, no target-vs-caller hierarchy check
await self._session.flush()
return member
File 3: src/praisonai-platform/praisonai_platform/api/deps.py, lines 54-73.
async def require_workspace_member(
workspace_id: str,
user: AuthIdentity = Depends(get_current_user),
session: AsyncSession = Depends(get_db),
min_role: str = "member", # <-- default that no route overrides
) -> AuthIdentity:
member_svc = MemberService(session)
has = await member_svc.has_role(workspace_id, user.id, min_role)
if not has:
raise HTTPException(status_code=403, detail="Not a member of this workspace or insufficient role")
user.workspace_id = workspace_id
return user
Why it's wrong: require_workspace_member was clearly designed to be tunable per-route — the min_role parameter is right there — but Depends(require_workspace_member) in FastAPI cannot pass arguments to a dependency, so every route resolves to the default "member". The author's intent is also evident in MemberService.has_role (member_service.py:80-96), which implements an owner > admin > member hierarchy that this endpoint should be enforcing. The endpoint uses none of it. The VALID_ROLES = {"owner", "admin", "member"} enum check (member_service.py:62) only validates the new role string is recognised, not that the caller has the right to assign it. As a result, a member can write {"role": "owner"} to their own membership row and become owner in one PATCH.
Exploit Chain
- Attacker registers an account and joins (or is invited to) any workspace
Was a "member" (the lowest privilege tier — typically anyone can be added by an owner during onboarding, or self-joins via an invite link). State: attacker has a JWT, is aMember(workspace_id=W, user_id=attacker, role="member"). - Attacker sends
PATCH /workspaces/W/members/<attacker_user_id>withAuthorization: Bearer <attacker_jwt>and body{"role": "owner"}. State: control flow entersupdate_member_role. require_workspace_member(W, attacker)runs. Its defaultmin_role="member"is satisfied because the attacker is a member. The dependency returns the attacker's identity. State: route handler proceeds with no further role gate.MemberService.update_role(W, attacker, "owner")runs.VALID_ROLESaccepts"owner".self.get(W, attacker)returns the attacker's existing member row. The next line,member.role = "owner", mutates the attacker's role in place.await self._session.flush()commits. State: attacker is nowMember(workspace_id=W, user_id=attacker, role="owner").- Attacker re-issues
GET /auth/me(or any owner-gated endpoint) and is now treated as workspace owner. State: full administrative control of the workspace, including the ability to add/remove members, change settings, delete the workspace, and exfiltrate everything via the agent/issue/project/comment IDORs that were filed as separate advisories. - Final state: starting from the lowest workspace privilege, the attacker holds owner of the workspace within one HTTP request. The same primitive also lets the attacker DEMOTE the legitimate owner by sending
PATCH /workspaces/W/members/<owner_user_id>with{"role": "member"}— owner lockout in two requests total.
Security Impact
Severity: sec-critical. CVSS 9.1: network attack, low complexity, low privileges (the lowest tier on the platform), no user interaction, scope changed (the privilege boundary the attacker crosses is the workspace owner, a different security principal), high confidentiality and integrity (full workspace control), no availability claim (the attacker can also DELETE the workspace via the companion delete_workspace advisory, but that is a separate finding).
Attacker capability: with one workspace-member token plus one PATCH request, the attacker becomes workspace owner. From there: add/remove any user as owner, change every workspace setting (including the settings JSON blob), demote the legitimate owner to "member", or chain into the companion delete_workspace advisory to wipe the workspace entirely. In multi-tenant SaaS deployments where any signup yields a member-level account in some default workspace, this is effectively pre-auth.
Preconditions: praisonai-platform is deployed multi-tenant (more than one workspace exists OR the deployment grants member access on signup); the attacker has any membership token in the target workspace.
Differential: source-inspection-verified end-to-end. The asymmetry between require_workspace_member's min_role parameter (which exists, defaults to "member", and is never overridden) and MemberService.has_role's clearly tiered owner > admin > member hierarchy (which exists but is never invoked with anything but the default) is the smoking gun. With the suggested fix below, the route resolves with min_role="owner", the attacker's member-level token fails the gate at the dependency, and the privilege escalation never reaches the service layer.
Suggested Fix
The fix has two parts. First, the route must resolve require_workspace_member with min_role="owner" (or at least "admin"). Second, MemberService.update_role should refuse to set a target's role higher than the caller's own role, so that an admin cannot accidentally produce another owner.
--- a/src/praisonai-platform/praisonai_platform/api/routes/workspaces.py
+++ b/src/praisonai-platform/praisonai_platform/api/routes/workspaces.py
@@ -115,11 +115,16 @@
+def _require_owner(workspace_id: str, user, session):
+ return require_workspace_member(workspace_id, user, session, min_role="owner")
+
@router.patch("/{workspace_id}/members/{user_id}", response_model=MemberResponse)
async def update_member_role(
workspace_id: str,
user_id: str,
body: MemberUpdate,
- user: AuthIdentity = Depends(require_workspace_member),
+ user: AuthIdentity = Depends(_require_owner),
session: AsyncSession = Depends(get_db),
):
member_svc = MemberService(session)
+ if not await member_svc.has_role(workspace_id, user.id, "owner"):
+ raise HTTPException(status_code=403, detail="Only owners can change member roles")
member = await member_svc.update_role(workspace_id, user_id, body.role)
Defence-in-depth in the service layer:
--- a/src/praisonai-platform/praisonai_platform/services/member_service.py
+++ b/src/praisonai-platform/praisonai_platform/services/member_service.py
@@ -55,7 +55,7 @@
- async def update_role(self, workspace_id: str, user_id: str, new_role: str) -> Optional[Member]:
+ async def update_role(self, workspace_id: str, caller_id: str, user_id: str, new_role: str) -> Optional[Member]:
"""Update a member's role."""
+ if not await self.has_role(workspace_id, caller_id, "owner"):
+ raise PermissionError("Only owners can update member roles")
if new_role not in VALID_ROLES:
raise ValueError(...)
The companion endpoints add_member, remove_member, delete_workspace, and update_workspace exhibit the same Depends(require_workspace_member) default-min-role pattern and are filed as their own advisories so each gets a separate CVE.
Affected versions
PyPI
| Package | Vulnerable | Fixed in |
|---|---|---|
| praisonai-platform | < 0.1.4 |
0.1.4 |
How to fix
Move to the fixed version or later with your package manager, update the lockfile, and run your tests. If the package only arrives through another dependency, upgrade that dependency first. If none of its releases pulls a fixed version yet, force the version with your package manager's override.
praisonai-platform PyPI → 0.1.4
- pip
pip install --upgrade "praisonai-platform>=0.1.4"- poetry
poetry add "praisonai-platform>=0.1.4"- uv
uv add "praisonai-platform>=0.1.4"
Severity breakdown
CVSS 3.1 9.6
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:N
- Attack vector
- Network
- Attack complexity
- Low
- Privileges required
- Low
- User interaction
- None
- Scope
- Changed
- Confidentiality
- High
- Integrity
- High
- Availability
- None
Weakness
CWE-269: Improper Privilege Management
The product does not properly assign, modify, track, or check privileges for an actor, creating an unintended sphere of control for that actor.
How to prevent it
- Architecture and Design, Operation: Very carefully manage the setting, management, and handling of privileges. Explicitly manage trust zones in the software.
- Architecture and Design: Follow the principle of least privilege when assigning access rights to entities in a software system.
- Architecture and Design: Consider following the principle of separation of privilege. Require multiple conditions to be met before permitting access to a system resource.
CWE-862: Missing Authorization
The product does not perform an authorization check when an actor attempts to access a resource or perform an action.
How to prevent it
- Architecture and Design: Divide the product into anonymous, normal, privileged, and administrative areas. Reduce the attack surface by carefully mapping roles with data and functionality. Use role-based access control (RBAC) [REF-229] to enforce the roles at the appropriate boundaries. Note that this approach may not protect against horizontal authorization, i.e., it will not protect a user from attacking others with the same role.
- Architecture and Design: Ensure that access control checks are performed related to the business logic. These checks may be different than the access control checks that are applied to more generic resources such as files, connections, processes, memory, and database records. For example, a database may restrict access for medical records to a specific database user, but each record might only be intended to be accessible to the patient and the patient's doctor [REF-7].
- Architecture and Design: Use a vetted library or framework that does not allow this weakness to occur or provides constructs that make this weakness easier to avoid. For example, consider using authorization frameworks such as the JAAS Authorization Framework [REF-233] and the OWASP ESAPI Access Control feature [REF-45].
- Architecture and Design: For web applications, make sure that the access control mechanism is enforced correctly at the server side on every page. Users should not be able to access any unauthorized functionality or information by simply requesting direct access to that page. One way to do this is to ensure that all pages containing sensitive information are not cached, and that all such pages restrict access to requests that are accompanied by an active and authenticated session token associated with a user who has the required permissions to access that page.
- System Configuration, Installation: Use the access control capabilities of your operating system and server environment and define your access control lists accordingly. Use a "default deny" policy when defining these ACLs.
References
Advisories
Packages
Other links
Timeline
- Published
- Last updated by the source
- Latest EPSS score
Related advisories
| Advisory | Severity | Why it's related |
|---|---|---|
| GHSA-xxgv-vgvj-qvxh PraisonAI: Platform members can rewrite shared labels and owner issue labels without owner/admin authorization | High | Also affects praisonai-platform |
| GHSA-c78w-2q4r-68r7 PraisonAI: Platform member PATCH routes allow owner resource rewrites and project lead reassignment delete bypass | High | Also affects praisonai-platform |
| GHSA-mxmx-rh57-jx58 PraisonAI: Platform members can delete owner issue dependencies through member-owned related issues | High | Also affects praisonai-platform |
| GHSA-2fjj-qqg8-fg7x praisonai-platform: Authorization Bypass Through User-Controlled Key | Medium | Also affects praisonai-platform |
| GHSA-cwj8-7gp2-ggcw praisonai-platform: default JWT signing secret 'dev-secret-change-me' enables token forgery | Critical | Also affects praisonai-platform |
| GHSA-f38v-77qj-h4jq praisonai-platform 0.1.4 still boots on the hardcoded JWT secret dev-secret-change-me (default-open production guard) | Critical | Also affects praisonai-platform |
| GHSA-rh39-9c67-59mh PraisonAI: Missing ownership check on DELETE endpoints allows members to delete others' content in Platform API | High | Also affects praisonai-platform |
| GHSA-7p8g-6c6g-h9w7 praisonai-platform: Agent endpoints accept any agent_id without workspace ownership check, cross-workspace read/update/delete IDOR | High | Also affects praisonai-platform |
MergeHarbor checks every repository's dependencies against this database, ranks what matters by exploitability and reachability, routes it to the owning team and opens the fix pull request.
Check your repos with MergeHarbor