PraisonAI Platform CVE-2026-47413
CRITICALSeverity by source
AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:N
Primary rating from GitHub Advisory · only source for this CVE.
CVSS VectorGitHub Advisory
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:N
Lifecycle Timeline
2DescriptionGitHub Advisory
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.
@router.post("/{workspace_id}/members", response_model=MemberResponse, status_code=status.HTTP_201_CREATED)
async def add_member(
workspace_id: str,
body: MemberAdd,
user: AuthIdentity = Depends(require_workspace_member),
# <-- BUG: defaults to min_role="member"
session: AsyncSession = Depends(get_db),
):
member_svc = MemberService(session)
member = await member_svc.add(workspace_id, body.user_id, body.role)
# <-- writes any (user, role)
return MemberResponse.model_validate(member)File 2: src/praisonai-platform/praisonai_platform/services/member_service.py, lines 26-38.
async def add(
self,
workspace_id: str,
user_id: str,
role: str = "member",
) -> Member:
"""Add a user to a workspace."""
if role not in VALID_ROLES:
# only validates the value
raise ValueError(f"Invalid role: {role}. Must be one of {VALID_ROLES}")
member = Member(workspace_id=workspace_id, user_id=user_id, role=role)
self._session.add(member)
# <-- BUG: no caller-permission check
await self._session.flush()
return memberWhy 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 Chain
- Attacker registers two accounts (or recruits a member account on the target workspace
W). Account A is an existing member ofW; Account B is a fresh signup the attacker controls (any account on the platform -auth/registeris open by default). State: attacker holds tokens for both A and B. - Attacker authenticates as Account A and POSTs
Authorization: Bearer <A_jwt>toPOST /workspaces/W/memberswith body{"user_id": "<B_user_id>", "role": "owner"}. State: control flow entersadd_member. require_workspace_member(W, A)passes (A is a member).MemberService.add(W, B, "owner")writes a new rowMember(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 Fix
--- a/src/praisonai-platform/praisonai_platform/api/routes/workspaces.py
+++ b/src/praisonai-platform/praisonai_platform/api/routes/workspaces.py
@@ -90,11 +90,15 @@
+def _require_workspace_owner(workspace_id: str, user, session):
+ return require_workspace_member(workspace_id, user, session, min_role="owner")
+
@router.post("/{workspace_id}/members", response_model=MemberResponse, status_code=status.HTTP_201_CREATED)
async def add_member(
workspace_id: str,
body: MemberAdd,
- user: AuthIdentity = Depends(require_workspace_member),
+ user: AuthIdentity = Depends(_require_workspace_owner),
session: AsyncSession = Depends(get_db),
):
member_svc = MemberService(session)
+ if body.role == "owner" and not await member_svc.has_role(workspace_id, user.id, "owner"):
+ raise HTTPException(status_code=403, detail="Only owners can add other owners")
member = await member_svc.add(workspace_id, body.user_id, body.role)The 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.
AnalysisAI
{workspace_id}/members. The endpoint enforces only the default member-tier gate and forwards the caller-supplied role directly to MemberService.add, which validates the role string but never checks the caller's permission to assign it. No public exploit identified at time of analysis, but a detailed exploit chain is documented in the vendor advisory.
Technical ContextAI
praisonai-platform is a Python/FastAPI multi-tenant workspace backend for the PraisonAI agent framework, using SQLAlchemy async sessions and a JWT-based AuthIdentity dependency model. The flaw is a CWE-269 Improper Privilege Management defect: the FastAPI dependency require_workspace_member supports a tunable min_role argument (deps.py:58) and the service layer already implements a tiered role check via MemberService.has_role (member_service.py:80-96), but the add_member route in workspaces.py:92-101 wires the dependency with its default min_role='member'. MemberService.add only validates that the requested role is in VALID_ROLES = {'owner','admin','member'} - a value-check, not an authorization check - so the request body's role field is written verbatim into the Member row.
RemediationAI
Vendor-released patch: upgrade praisonai-platform to 0.1.4 or later, which is the fixed version listed in GHSA-8g2p-pqm3-fcfh. The structural fix is to change the add_member dependency from Depends(require_workspace_member) to a min_role='owner' variant and to additionally guard owner-role assignment with an explicit MemberService.has_role check, as shown in the advisory's suggested diff. If immediate upgrade is not possible, compensating controls include placing an authenticated reverse proxy or WAF rule in front of the API that blocks or 403s POST /workspaces/*/members for non-owner principals (trade-off: requires the proxy to parse the JWT and consult workspace role state, otherwise it will either over-block or be bypassable), restricting workspace membership to fully trusted users until patched (trade-off: defeats the purpose of multi-tenant collaboration), or disabling open self-registration on auth/register so attackers cannot freely create the secondary identity used as the elevation target (trade-off: only narrows the attack to insiders, since an existing member account is still sufficient to grief or hand owner rights to any known user_id).
Wazuh SIEM platform versions 4.4.0 through 4.9.0 contain an unsafe deserialization vulnerability in the DistributedAPI t
BentoML version 1.4.2 and earlier contains an unauthenticated remote code execution vulnerability through insecure deser
pgAdmin 4 contains critical remote code execution vulnerabilities in the Query Tool download and Cloud Deployment endpoi
The renderLocalView function in render/views.py in graphite-web in Graphite 0.9.5 through 0.9.10 uses the pickle Python
BentoML is a Python library for building online serving systems optimized for AI apps and model inference. Rated critica
OpenSSL before 0.9.8za, 1.0.0 before 1.0.0m, and 1.0.1 before 1.0.1h does not properly restrict processing of ChangeCiph
pyLoad download manager version prior to 0.5.0b3.dev77 exposes the Flask SECRET_KEY through an unauthenticated endpoint.
In Mercurial before 4.1.3, "hg serve --stdio" allows remote authenticated users to launch the Python debugger, and conse
Unauthenticated remote code execution in Marimo ≤0.20.4 allows attackers to execute arbitrary system commands via the `/
pyLoad is the free and open-source Download Manager written in pure Python. Rated medium severity (CVSS 5.3), this vulne
Langflow (a visual LLM pipeline builder) contains a critical unauthenticated code execution vulnerability (CVE-2026-3301
Cross-user flow execution in Langflow (< 1.9.1) lets any authenticated API-key holder run another user's flow by passing
Same weakness CWE-269 – Improper Privilege Management
View allSame technique Privilege Escalation
View allShare
External POC / Exploit Code
Leaving vuln.today
GHSA-8g2p-pqm3-fcfh