praisonai-platform: Missing authorization on member removal enables full workspace takeover by any user regardless of role
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
- Attacker is a member of workspace
Wwith role "member". State: attacker holds JWT. - Attacker enumerates the workspace owner's
user_idviaGET /workspaces/W/members(list_members has the same default-member gate, separate finding). Owner UUIDO_idis now known. State: attacker holdsO_id. - Attacker sends
DELETE /workspaces/W/members/O_idwithAuthorization: Bearer <attacker_jwt>. State: control flow entersremove_member. 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.- Owner attempts
GET /workspaces/W/...andrequire_workspace_member(W, O_id)returns 403. State: legitimate owner is now locked out of their own workspace. - Combined with the
update_member_rolecompanion advisory, the attacker first promotes themselves to owner, then removes the legitimate owner, then has uncontested control. Combined withdelete_workspace, the attacker wipes the workspace after kicking the owner. - 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.
| Package | Affected versions | Patched versions |
|---|---|---|
praisonai-platformPyPI | < 0.1.4 | 0.1.4 |
Affected products
1Patches
18b72cec167ecMerge pull request #168 from MervinPraison/develop
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("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/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
2News mentions
0No linked articles in our index yet.