CVE-2026-47416: praisonai-platform: member-to-owner privilege escalation

GHSA-c2m8-4gcg-v22g CRITICAL
Published May 29, 2026
CISO Take

PraisonAI Platform (<=0.1.2) contains a critical privilege escalation vulnerability where any workspace member can promote themselves to owner with a single HTTP PATCH request due to a FastAPI dependency misconfiguration — the authorization gate always resolves to the minimum "member" role regardless of the intended permission model. No public exploit code is available and it is not in CISA KEV, but with a CVSS score of 9.1, trivial exploitation requiring only one authenticated request, and full workspace takeover potential — including the ability to demote legitimate owners and chain into companion workspace-deletion advisories — urgency is high for any multi-tenant deployment where signup yields automatic workspace membership. Organizations running praisonai-platform must upgrade immediately to version 0.1.4; in the interim, block or WAF-gate PATCH requests to /workspaces/*/members/* for non-owner sessions.

Sources: NVD GitHub Advisory ATLAS

What is the risk?

CRITICAL. CVSS 9.1 (AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:N). Exploitation requires only a valid member-tier token obtainable through normal onboarding, invite links, or free signup. No brute force, social engineering, or AI/ML knowledge is required — a single HTTP PATCH completes the escalation in one round-trip. The companion advisory family (add_member, remove_member, delete_workspace, update_workspace) shares the same root cause (Depends(require_workspace_member) defaulting to min_role="member"), compounding blast radius significantly. In multi-tenant deployments this is effectively pre-authenticated privilege escalation for any user who can reach the API.

Attack Kill Chain

Initial Access
Attacker obtains a valid workspace member token by registering an account and joining the target workspace via invite link, public signup, or self-enrollment — no elevated privilege required.
AML.T0012
Privilege Escalation
Attacker issues a single PATCH /workspaces/{id}/members/{self_id} with body {"role":"owner"}, exploiting the FastAPI dependency misconfiguration that resolves the authorization gate to min_role="member" for every route.
AML.T0049
Owner Lockout
Attacker sends a second PATCH demoting the legitimate owner to "member" role, preventing recovery while cementing persistent owner-level control over the workspace.
AML.T0081
Impact
Attacker exfiltrates all AI agent configurations, LLM provider API keys, system prompts, tool definitions, and workspace data, then optionally invokes the companion delete_workspace endpoint to destroy forensic evidence.
AML.T0053

What systems are affected?

Package Ecosystem Vulnerable Range Patched
praisonai-platform pip <= 0.1.2 0.1.4
1 dependents 86% patched ~0d to patch Full package profile →

Do you use praisonai-platform? You're affected.

Severity & Risk

CVSS 3.1
9.6 / 10
EPSS
N/A
Exploitation Status
No known exploitation
Sophistication
Trivial

Attack Surface

AV AC PR UI S C I A
AV Network
AC Low
PR Low
UI None
S Changed
C High
I High
A None

What should I do?

5 steps
  1. PATCH IMMEDIATELY

    Upgrade praisonai-platform to >=0.1.4, which corrects the FastAPI dependency misconfiguration by introducing a dedicated _require_owner closure and adds caller-role enforcement inside MemberService.update_role.

  2. DETECTION

    Query application access logs for PATCH /workspaces/*/members/* requests where the requesting user's prior role was "member" and the response was HTTP 200 with a body indicating role "owner" or "admin" — this is the exact exploit signature.

  3. INVESTIGATION

    If exploitation is suspected, audit your members table for unexpected owner/admin promotions, especially where the previous owner was simultaneously demoted. Check timestamps for sub-second sequences (two-request lockout pattern).

  4. WORKAROUND (if immediate patching is not feasible): Deploy a WAF or API gateway rule blocking PATCH /workspaces/*/members/* for any session token whose current role is below admin; or disable the endpoint until the patch is applied.

  5. AUDIT COMPANION ENDPOINTS

    Per the advisory, add_member, remove_member, delete_workspace, and update_workspace share the same Depends(require_workspace_member) default pattern — review each for equivalent authorization gaps pending their own CVE assignments.

Classification

Compliance Impact

This CVE is relevant to:

EU AI Act
Article 15 - Accuracy, robustness and cybersecurity
ISO 42001
A.6.1.2 - Information security roles and responsibilities in AI systems
NIST AI RMF
GOVERN 6.1 - Policies and procedures for AI risk response
OWASP LLM Top 10
LLM06 - Excessive Agency

Frequently Asked Questions

What is CVE-2026-47416?

PraisonAI Platform (<=0.1.2) contains a critical privilege escalation vulnerability where any workspace member can promote themselves to owner with a single HTTP PATCH request due to a FastAPI dependency misconfiguration — the authorization gate always resolves to the minimum "member" role regardless of the intended permission model. No public exploit code is available and it is not in CISA KEV, but with a CVSS score of 9.1, trivial exploitation requiring only one authenticated request, and full workspace takeover potential — including the ability to demote legitimate owners and chain into companion workspace-deletion advisories — urgency is high for any multi-tenant deployment where signup yields automatic workspace membership. Organizations running praisonai-platform must upgrade immediately to version 0.1.4; in the interim, block or WAF-gate PATCH requests to /workspaces/*/members/* for non-owner sessions.

Is CVE-2026-47416 actively exploited?

No confirmed active exploitation of CVE-2026-47416 has been reported, but organizations should still patch proactively.

How to fix CVE-2026-47416?

1. PATCH IMMEDIATELY: Upgrade praisonai-platform to >=0.1.4, which corrects the FastAPI dependency misconfiguration by introducing a dedicated _require_owner closure and adds caller-role enforcement inside MemberService.update_role. 2. DETECTION: Query application access logs for PATCH /workspaces/*/members/* requests where the requesting user's prior role was "member" and the response was HTTP 200 with a body indicating role "owner" or "admin" — this is the exact exploit signature. 3. INVESTIGATION: If exploitation is suspected, audit your members table for unexpected owner/admin promotions, especially where the previous owner was simultaneously demoted. Check timestamps for sub-second sequences (two-request lockout pattern). 4. WORKAROUND (if immediate patching is not feasible): Deploy a WAF or API gateway rule blocking PATCH /workspaces/*/members/* for any session token whose current role is below admin; or disable the endpoint until the patch is applied. 5. AUDIT COMPANION ENDPOINTS: Per the advisory, add_member, remove_member, delete_workspace, and update_workspace share the same Depends(require_workspace_member) default pattern — review each for equivalent authorization gaps pending their own CVE assignments.

What systems are affected by CVE-2026-47416?

This vulnerability affects the following AI/ML architecture patterns: agent frameworks, multi-tenant AI platforms, AI agent orchestration.

What is the CVSS score for CVE-2026-47416?

CVE-2026-47416 has a CVSS v3.1 base score of 9.6 (CRITICAL).

AI Security Impact

Affected AI Architectures

agent frameworksmulti-tenant AI platformsAI agent orchestration

MITRE ATLAS Techniques

AML.T0012 Valid Accounts
AML.T0049 Exploit Public-Facing Application
AML.T0053 AI Agent Tool Invocation
AML.T0081 Modify AI Agent Configuration

Compliance Controls Affected

EU AI Act: Article 15
ISO 42001: A.6.1.2
NIST AI RMF: GOVERN 6.1
OWASP LLM Top 10: LLM06

Technical Details

Original Advisory

## 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. ```python @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. ```python 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. ```python 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 1. Attacker registers an account and joins (or is invited to) any workspace `W` as 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 a `Member(workspace_id=W, user_id=attacker, role="member")`. 2. Attacker sends `PATCH /workspaces/W/members/<attacker_user_id>` with `Authorization: Bearer <attacker_jwt>` and body `{"role": "owner"}`. State: control flow enters `update_member_role`. 3. `require_workspace_member(W, attacker)` runs. Its default `min_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. 4. `MemberService.update_role(W, attacker, "owner")` runs. `VALID_ROLES` accepts `"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 now `Member(workspace_id=W, user_id=attacker, role="owner")`. 5. 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. 6. 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. ```diff --- 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: ```diff --- 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.

Exploitation Scenario

An adversary targeting an organization using PraisonAI Platform for internal AI agent orchestration registers a free account and receives an invite to the target workspace as a standard member. Using their valid JWT, they issue one HTTP request: PATCH /workspaces/{target_workspace_id}/members/{their_own_user_id} with body {"role": "owner"}. FastAPI resolves the dependency with the default min_role="member" (satisfied), and MemberService.update_role writes the new role with no caller check. The attacker is now workspace owner in a single round-trip. In a follow-up request they demote the legitimate owner to "member" to prevent recovery. They then exfiltrate all AI agent configurations — including LLM provider API keys, system prompts, and tool definitions stored in workspace settings — and enumerate agent conversation history and connected RAG data sources. Finally they invoke the companion delete_workspace endpoint to destroy forensic evidence before the organization detects the compromise.

CVSS Vector

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:N

Timeline

Published
May 29, 2026
Last Modified
May 29, 2026
First Seen
May 30, 2026

Related Vulnerabilities