VYPR
High severityGHSA Advisory· Published May 29, 2026· Updated May 29, 2026

praisonai-platform: Missing authorization on member removal enables full workspace takeover by any user regardless of role

CVE-2026-47409

Description

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

  1. Attacker is a member of workspace W with role "member". State: attacker holds JWT.
  2. Attacker enumerates the workspace owner's user_id via GET /workspaces/W/members (list_members has the same default-member gate, separate finding). Owner UUID O_id is now known. State: attacker holds O_id.
  3. Attacker sends DELETE /workspaces/W/members/O_id with Authorization: Bearer <attacker_jwt>. State: control flow enters remove_member.
  4. 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.
  5. Owner attempts GET /workspaces/W/... and require_workspace_member(W, O_id) returns 403. State: legitimate owner is now locked out of their own workspace.
  6. Combined with the update_member_role companion advisory, the attacker first promotes themselves to owner, then removes the legitimate owner, then has uncontested control. Combined with delete_workspace, the attacker wipes the workspace after kicking the owner.
  7. 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.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Any workspace member can remove the owner via DELETE endpoint due to missing authorization checks, enabling permanent lockout.

Vulnerability

The DELETE /workspaces/{workspace_id}/members/{user_id} endpoint in praisonai-platform lacks caller-role and target-role authorization checks. The route uses require_workspace_member with default min_role="member", and MemberService.remove() performs deletion without verifying the caller's permissions or protecting the workspace owner. This allows any workspace member to remove any other member, including the owner. The affected code is in src/praisonai-platform/praisonai_platform/api/routes/workspaces.py (lines 130-140) and services/member_service.py (lines 71-78). All versions prior to a fix are vulnerable [1][2].

Exploitation

An attacker must be an authenticated member of the target workspace (any role). They can send a single DELETE request to the endpoint with the workspace ID and the owner's user ID. No additional privileges or user interaction are required. The request is processed without any role check, and the owner is immediately removed from the workspace [1][2].

Impact

Successful exploitation removes the workspace owner from the workspace, locking them out permanently (unless re-added by another member with sufficient privileges). This results in a denial of service for the owner and potential loss of administrative control over the workspace. No data confidentiality or integrity impact is described, but the owner loses access [1][2].

Mitigation

The advisory recommends adding a caller-role check (e.g., requiring admin or owner role) and a guard that prevents removal of the last owner. As of the publication date (2026-05-29), no specific patched version has been released. Users should monitor the repository for updates and apply the fix as soon as it becomes available [1][2].

AI Insight generated on May 29, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
praisonai-platformPyPI
< 0.1.40.1.4

Affected products

1

Patches

1
8b72cec167ec

Merge pull request #168 from MervinPraison/develop

https://github.com/MervinPraison/PraisonAIMervin PraisonOct 8, 2024Fixed in 0.1.3via release-tag
6 files changed · +25 18
  • Dockerfile+1 1 modified
    @@ -1,6 +1,6 @@
     FROM python:3.11-slim
     WORKDIR /app
     COPY . .
    -RUN pip install flask praisonai==0.1.2 gunicorn markdown
    +RUN pip install flask praisonai==0.1.3 gunicorn markdown
     EXPOSE 8080
     CMD ["gunicorn", "-b", "0.0.0.0:8080", "api:app"]
    
  • docs/api/praisonai/deploy.html+1 1 modified
    @@ -110,7 +110,7 @@ <h2 id="raises">Raises</h2>
                 file.write(&#34;FROM python:3.11-slim\n&#34;)
                 file.write(&#34;WORKDIR /app\n&#34;)
                 file.write(&#34;COPY . .\n&#34;)
    -            file.write(&#34;RUN pip install flask praisonai==0.1.2 gunicorn markdown\n&#34;)
    +            file.write(&#34;RUN pip install flask praisonai==0.1.3 gunicorn markdown\n&#34;)
                 file.write(&#34;EXPOSE 8080\n&#34;)
                 file.write(&#39;CMD [&#34;gunicorn&#34;, &#34;-b&#34;, &#34;0.0.0.0:8080&#34;, &#34;api:app&#34;]\n&#39;)
                 
    
  • praisonai/deploy.py+1 1 modified
    @@ -56,7 +56,7 @@ def create_dockerfile(self):
                 file.write("FROM python:3.11-slim\n")
                 file.write("WORKDIR /app\n")
                 file.write("COPY . .\n")
    -            file.write("RUN pip install flask praisonai==0.1.2 gunicorn markdown\n")
    +            file.write("RUN pip install flask praisonai==0.1.3 gunicorn markdown\n")
                 file.write("EXPOSE 8080\n")
                 file.write('CMD ["gunicorn", "-b", "0.0.0.0:8080", "api:app"]\n')
                 
    
  • praisonai.rb+1 1 modified
    @@ -3,7 +3,7 @@ class Praisonai < Formula
       
         desc "AI tools for various AI applications"
         homepage "https://github.com/MervinPraison/PraisonAI"
    -    url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/0.1.2.tar.gz"
    +    url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/0.1.3.tar.gz"
         sha256 "1828fb9227d10f991522c3f24f061943a254b667196b40b1a3e4a54a8d30ce32"  # Replace with actual SHA256 checksum
         license "MIT"
       
    
  • praisonai/ui/realtime.py+20 13 modified
    @@ -6,7 +6,6 @@
     
     from openai import AsyncOpenAI
     import chainlit as cl
    -from chainlit.logger import logger
     from chainlit.input_widget import TextInput
     from chainlit.types import ThreadDict
     
    @@ -16,6 +15,25 @@
     import chainlit.data as cl_data
     from literalai.helper import utc_now
     import json
    +import logging
    +import importlib.util
    +from importlib import import_module
    +from pathlib import Path
    +
    +# Set up logging
    +logger = logging.getLogger(__name__)
    +log_level = os.getenv("LOGLEVEL", "INFO").upper()
    +logger.handlers = []
    +
    +# Set up logging to console
    +console_handler = logging.StreamHandler()
    +console_handler.setLevel(log_level)
    +console_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
    +console_handler.setFormatter(console_formatter)
    +logger.addHandler(console_handler)
    +
    +# Set the logging level for the logger
    +logger.setLevel(log_level)
     
     # Set up CHAINLIT_AUTH_SECRET
     CHAINLIT_AUTH_SECRET = os.getenv("CHAINLIT_AUTH_SECRET")
    @@ -144,19 +162,8 @@ def load_setting(key: str) -> str:
     
     client = AsyncOpenAI()
     
    -# Add these new imports and code
    -import importlib.util
    -import logging
    -from importlib import import_module
    -from pathlib import Path
    -
    -# Set up logging
    -logging.basicConfig(level=logging.INFO)
    -logger = logging.getLogger(__name__)
    -
     # Try to import tools from the root directory
    -root_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
    -tools_path = os.path.join(root_dir, 'tools.py')
    +tools_path = os.path.join(os.getcwd(), 'tools.py')
     logger.info(f"Tools path: {tools_path}")
     
     def import_tools_from_file(file_path):
    
  • pyproject.toml+1 1 modified
    @@ -1,6 +1,6 @@
     [tool.poetry]
     name = "PraisonAI"
    -version = "0.1.2"
    +version = "0.1.3"
     description = "PraisonAI application combines AutoGen and CrewAI or similar frameworks into a low-code solution for building and managing multi-agent LLM systems, focusing on simplicity, customization, and efficient human-agent collaboration."
     authors = ["Mervin Praison"]
     license = ""
    

Vulnerability mechanics

Root cause

"The `DELETE /workspaces/{workspace_id}/members/{user_id}` endpoint gates access only on workspace membership (default `min_role="member"`) and performs the deletion without any caller-role check, target-role check, or last-owner protection."

Attack vector

An attacker who is a member of a workspace can issue a `DELETE /workspaces/{workspace_id}/members/{user_id}` request targeting the workspace owner's user_id. The `require_workspace_member` dependency passes because the attacker is a member, and `MemberService.remove()` deletes the owner's membership row without any role check [ref_id=1][ref_id=2]. The owner's user_id can be obtained via the `GET /workspaces/{workspace_id}/members` endpoint, which also uses the same default-member gate [ref_id=1]. This results in permanent owner lockout — the legitimate owner receives 403 on subsequent workspace requests and has no recourse without database-level intervention [ref_id=1][ref_id=2].

Affected code

The vulnerability is in `src/praisonai-platform/praisonai_platform/api/routes/workspaces.py`, lines 130-140, where the `remove_member` endpoint uses `require_workspace_member` (default `min_role="member"`) instead of an owner-level gate. The actual deletion is performed by `MemberService.remove()` in `src/praisonai-platform/praisonai_platform/services/member_service.py`, lines 71-78, which executes the delete without any caller-permission check or last-owner protection [ref_id=1][ref_id=2].

What the fix does

The suggested fix changes the dependency from `require_workspace_member` to `_require_workspace_owner`, ensuring only users with the owner role can call the endpoint [ref_id=1][ref_id=2]. It also adds a guard that checks whether the target member is an owner and, if so, refuses to remove the last owner by returning a 409 Conflict [ref_id=1][ref_id=2]. This prevents both unauthorized member removal and permanent owner lockout.

Preconditions

  • configThe praisonai-platform is deployed in a multi-tenant configuration
  • authAttacker holds a valid workspace membership token with at least the 'member' role
  • inputThe workspace owner's user_id is discoverable (e.g., via the list_members endpoint)
  • networkThe attacker can reach the DELETE endpoint over the network

Generated on May 29, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

2

News mentions

0

No linked articles in our index yet.