praisonai-platform: Any workspace member can add arbitrary user as owner via POST /workspaces/{id}/members
Description
Any workspace member can add an arbitrary user as owner due to missing permission check in MemberService.add, enabling privilege escalation.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Any workspace member can add an arbitrary user as owner due to missing permission check in MemberService.add, enabling privilege escalation.
Vulnerability
The vulnerability is a missing authorization check in the POST /workspaces/{workspace_id}/members endpoint of the PraisonAI platform. The route uses Depends(require_workspace_member) which defaults to min_role="member", allowing any existing workspace member to invoke the endpoint. The request body's user_id and role are passed directly to MemberService.add(workspace_id, user_id, role), which only validates that the role is one of {"owner", "admin", "member"} but does not verify that the caller has permission to assign that role. This flaw exists in src/praisonai-platform/praisonai_platform/api/routes/workspaces.py lines 92-101 and services/member_service.py lines 26-38 [1][2].
Exploitation
An attacker who already holds a member-level token for a workspace can send a single POST request to /workspaces/{workspace_id}/members with a JSON body containing user_id (any known user, including a second attacker-controlled account) and role set to "owner". The attacker does not need any additional privileges; the endpoint processes the request without checking whether the caller is allowed to promote or add users at that role level [1][2].
Impact
Successful exploitation results in the attacker gaining an alternate identity with the owner role within the same workspace. This bypasses all owner-only operations that would otherwise restrict the attacker's original member account, leading to full workspace control, including the ability to manage other members, delete the workspace, and access sensitive workspace data [1][2].
Mitigation
As of the publication date (2026-06-01), no patched version has been released. The GitHub Advisory suggests that the fix should involve adding a permission check in MemberService.add to ensure the caller has sufficient privileges (e.g., min_role="admin" or "owner") to assign the requested role, and that the endpoint's dependency should be updated accordingly. Workarounds are not documented; users should monitor the GitHub repository for a patch [1][2].
AI Insight generated on Jun 1, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
1Patches
28c4ab718aeebPraisonAI Call API Release
7 files changed · +43 −14
Dockerfile+1 −1 modified@@ -1,6 +1,6 @@ FROM python:3.11-slim WORKDIR /app COPY . . -RUN pip install flask praisonai==0.1.3 gunicorn markdown +RUN pip install flask praisonai==0.1.4 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.3 gunicorn markdown\n") + file.write("RUN pip install flask praisonai==0.1.4 gunicorn markdown\n") file.write("EXPOSE 8080\n") file.write('CMD ["gunicorn", "-b", "0.0.0.0:8080", "api:app"]\n')
poetry.lock+20 −2 modified@@ -6258,6 +6258,24 @@ cffi = ">=1.4.1" docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"] tests = ["hypothesis (>=3.27.0)", "pytest (>=3.2.1,!=3.3.0)"] +[[package]] +name = "pyngrok" +version = "7.2.0" +description = "A Python wrapper for ngrok." +optional = true +python-versions = ">=3.8" +files = [ + {file = "pyngrok-7.2.0-py3-none-any.whl", hash = "sha256:1e96ab1229736e2e030fa8975805ab1fa9e463178f83337fc07fdd2b4e8dbed6"}, + {file = "pyngrok-7.2.0.tar.gz", hash = "sha256:4e43af9b2f21ceed8d213797028fe8823003f185b49792e4d383302365c81515"}, +] + +[package.dependencies] +PyYAML = ">=5.1" + +[package.extras] +dev = ["coverage[toml]", "flake8", "flake8-pyproject", "pep8-naming", "psutil"] +docs = ["Sphinx", "mypy", "sphinx-autodoc-typehints (==1.25.2)", "sphinx-notfound-page", "sphinx-substitution-extensions", "types-PyYAML"] + [[package]] name = "pyparsing" version = "3.2.0" @@ -8857,7 +8875,7 @@ type = ["pytest-mypy"] agentops = ["agentops"] anthropic = ["langchain-anthropic"] api = ["flask"] -call = ["fastapi", "flaml", "python-dotenv", "twilio", "typer", "uvicorn", "websockets"] +call = ["fastapi", "flaml", "pyngrok", "python-dotenv", "rich", "twilio", "typer", "uvicorn", "websockets"] chat = ["aiosqlite", "chainlit", "crawl4ai", "greenlet", "litellm", "tavily-python"] code = ["aiosqlite", "chainlit", "crawl4ai", "greenlet", "litellm", "tavily-python"] cohere = ["langchain-cohere"] @@ -8871,4 +8889,4 @@ ui = ["chainlit"] [metadata] lock-version = "2.0" python-versions = ">=3.10,<3.13" -content-hash = "74602750ef14d7040dad02842980b2eebbf0ac8c1f4324627602764578814a5a" +content-hash = "d246ccacd08ba3599a7b22e222e784396df2385dc76532b56e86bfccaf7522c0"
praisonai/api/call.py+16 −6 modified@@ -10,6 +10,8 @@ from dotenv import load_dotenv import typer import uvicorn +from pyngrok import ngrok +from rich import print load_dotenv() @@ -151,17 +153,26 @@ async def send_session_update(openai_ws): print('Sending session update:', json.dumps(session_update)) await openai_ws.send(json.dumps(session_update)) -def run_server(port: int): +def run_server(port: int, use_ngrok: bool = False): """Run the FastAPI server using uvicorn.""" + if use_ngrok: + public_url = ngrok.connect(port).public_url + # print(f"Ngrok tunnel established: {public_url}") + print(f"Praison AI Voice URL: {public_url}/call") + print(f"Starting Praison AI Call Server on port {port}...") - uvicorn.run(app, host="0.0.0.0", port=port) + uvicorn.run(app, host="0.0.0.0", port=port, log_level="warning") app_cli = typer.Typer() @app_cli.command() -def main(port: int = typer.Option(8090, help="Port to run the server on")): +def main( + port: int = typer.Option(8090, help="Port to run the server on"), + ngrok: bool = typer.Option(False, help="Use ngrok to expose the server") +): """Run the Praison AI Call Server.""" - print(f"Received port value: {port}") # Debug print + # print(f"Received port value: {port}") # Debug print + # print(f"Use ngrok: {ngrok}") # Debug print # Extract the actual port value from the OptionInfo object if isinstance(port, typer.models.OptionInfo): @@ -170,9 +181,8 @@ def main(port: int = typer.Option(8090, help="Port to run the server on")): port_value = port port_int = int(port_value) - print(f"Using port: {port_int}") # Debug print - run_server(port=port_int) + run_server(port=port_int, use_ngrok=ngrok) if __name__ == "__main__": app_cli()
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.3 gunicorn markdown\n") + file.write("RUN pip install flask praisonai==0.1.4 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.3.tar.gz" + url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/0.1.4.tar.gz" sha256 "1828fb9227d10f991522c3f24f061943a254b667196b40b1a3e4a54a8d30ce32" # Replace with actual SHA256 checksum license "MIT"
pyproject.toml+3 −2 modified@@ -1,6 +1,6 @@ [tool.poetry] name = "PraisonAI" -version = "0.1.3" +version = "0.1.4" 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 = "" @@ -44,6 +44,7 @@ uvicorn = {version = ">=0.20.0", optional = true} python-dotenv = {version = ">=0.19.0", optional = true} typer = {version = ">=0.9.0", optional = true} flaml = {version = ">=2.3.1", extras = ["automl"], optional = true} +pyngrok = {version = ">=1.4.0", optional = true} [tool.poetry.group.docs.dependencies] mkdocs = "*" @@ -118,7 +119,7 @@ chat = ["chainlit", "litellm", "aiosqlite", "greenlet", "tavily-python", "crawl4 code = ["chainlit", "litellm", "aiosqlite", "greenlet", "tavily-python", "crawl4ai"] train = ["setup-conda-env"] realtime = ["chainlit", "litellm", "aiosqlite", "greenlet", "tavily-python", "crawl4ai", "websockets", "plotly", "yfinance", "duckduckgo_search"] -call = ["twilio", "fastapi", "uvicorn", "websockets", "python-dotenv", "typer", "flaml"] +call = ["twilio", "fastapi", "uvicorn", "websockets", "python-dotenv", "typer", "flaml", "pyngrok", "rich"] [tool.poetry-dynamic-versioning] enable = true
8cffa23da4dbPraison AI Call basics
6 files changed · +1212 −1162
docs/ui/ui.md+4 −0 modified@@ -88,3 +88,7 @@ streamlit run app.py ### Manual Model Output  + +## PraisonAI Call + +To use the PraisonAI Call feature:
poetry.lock+1162 −1156 modifiedpraisonai/api/call.py+26 −4 modified@@ -8,6 +8,8 @@ from fastapi.websockets import WebSocketDisconnect from twilio.twiml.voice_response import VoiceResponse, Connect, Say, Stream from dotenv import load_dotenv +import typer +import uvicorn load_dotenv() @@ -29,7 +31,6 @@ app = FastAPI() - if not OPENAI_API_KEY: raise ValueError('Missing the OpenAI API key. Please set it in the .env file.') @@ -50,7 +51,6 @@ async def index_page(): async def handle_incoming_call(request: Request): """Handle incoming call and return TwiML response to connect to Media Stream.""" response = VoiceResponse() - # <Say> punctuation to improve text-to-speech flow response.say("") response.pause(length=1) response.say("O.K. you can start talking!") @@ -151,6 +151,28 @@ async def send_session_update(openai_ws): print('Sending session update:', json.dumps(session_update)) await openai_ws.send(json.dumps(session_update)) +def run_server(port: int): + """Run the FastAPI server using uvicorn.""" + print(f"Starting Praison AI Call Server on port {port}...") + uvicorn.run(app, host="0.0.0.0", port=port) + +app_cli = typer.Typer() + +@app_cli.command() +def main(port: int = typer.Option(8090, help="Port to run the server on")): + """Run the Praison AI Call Server.""" + print(f"Received port value: {port}") # Debug print + + # Extract the actual port value from the OptionInfo object + if isinstance(port, typer.models.OptionInfo): + port_value = port.default + else: + port_value = port + + port_int = int(port_value) + print(f"Using port: {port_int}") # Debug print + + run_server(port=port_int) + if __name__ == "__main__": - import uvicorn - uvicorn.run(app, host="0.0.0.0", port=PORT) + app_cli()
praisonai/cli.py+10 −1 modified@@ -16,6 +16,8 @@ import shutil import subprocess import logging +import importlib +import praisonai.api.call as call_module logging.basicConfig(level=os.environ.get('LOGLEVEL', 'INFO'), format='%(asctime)s - %(levelname)s - %(message)s') try: @@ -134,6 +136,10 @@ def main(self): self.create_realtime_interface() return + if getattr(args, 'call', False): + call_module.main() + return + if args.agent_file == 'train': package_root = os.path.dirname(os.path.abspath(__file__)) config_yaml_destination = os.path.join(os.getcwd(), 'config.yaml') @@ -261,6 +267,7 @@ def parse_args(self): parser.add_argument("--ollama", type=str, help="Ollama model name") parser.add_argument("--dataset", type=str, help="Dataset name for training", default="yahma/alpaca-cleaned") parser.add_argument("--realtime", action="store_true", help="Start the realtime voice interaction interface") + parser.add_argument("--call", action="store_true", help="Start the PraisonAI Call server") args, unknown_args = parser.parse_known_args() if unknown_args and unknown_args[0] == '-b' and unknown_args[1] == 'api:app': @@ -277,6 +284,8 @@ def parse_args(self): args.code = True if args.agent_file == 'realtime': args.realtime = True + if args.agent_file == 'call': + args.call = True return args @@ -448,4 +457,4 @@ def create_realtime_interface(self): if __name__ == "__main__": praison_ai = PraisonAI() - praison_ai.main() \ No newline at end of file + praison_ai.main()
pyproject.toml+9 −1 modified@@ -38,6 +38,12 @@ websockets = {version = ">=12.0", optional = true} plotly = {version = ">=5.24.0", optional = true} yfinance = {version = ">=0.2.44", optional = true} duckduckgo_search = {version = ">=6.3.0", optional = true} +twilio = {version = ">=7.0.0", optional = true} +fastapi = {version = ">=0.95.0", optional = true} +uvicorn = {version = ">=0.20.0", optional = true} +python-dotenv = {version = ">=0.19.0", optional = true} +typer = {version = ">=0.9.0", optional = true} +flaml = {version = ">=2.3.1", extras = ["automl"], optional = true} [tool.poetry.group.docs.dependencies] mkdocs = "*" @@ -97,6 +103,7 @@ build-backend = "poetry.core.masonry.api" praisonai = "praisonai.__main__:main" setup-conda-env = "setup.setup_conda_env:main" post-install = "setup.post_install:main" +praisonai-call = "praisonai.api.call:main" [tool.poetry.extras] ui = ["chainlit"] @@ -111,6 +118,7 @@ chat = ["chainlit", "litellm", "aiosqlite", "greenlet", "tavily-python", "crawl4 code = ["chainlit", "litellm", "aiosqlite", "greenlet", "tavily-python", "crawl4ai"] train = ["setup-conda-env"] realtime = ["chainlit", "litellm", "aiosqlite", "greenlet", "tavily-python", "crawl4ai", "websockets", "plotly", "yfinance", "duckduckgo_search"] +call = ["twilio", "fastapi", "uvicorn", "websockets", "python-dotenv", "typer", "flaml"] [tool.poetry-dynamic-versioning] enable = true @@ -119,4 +127,4 @@ style = "semver" [tool.poetry.build] generate-setup-file = false -script = "praisonai/setup/post_install.py" \ No newline at end of file +script = "praisonai/setup/post_install.py"
README.md+1 −0 modified@@ -58,6 +58,7 @@ Praison AI, leveraging both AutoGen and CrewAI or any other agent framework, rep | **PraisonAI Chat** | `pip install "praisonai[chat]"` | | **PraisonAI Train** | `pip install "praisonai[train]"` | | **PraisonAI Realtime** | `pip install "praisonai[realtime]"` | +| **PraisonAI Call** | `pip install "praisonai[call]"` | ## Key Features
Vulnerability mechanics
Root cause
"The `POST /workspaces/{workspace_id}/members` endpoint gates access only on workspace membership (default `min_role="member"`) and `MemberService.add` performs no caller-permission check, allowing any member to add any user as workspace owner."
Attack vector
An attacker who holds a member-level token for a target workspace sends a POST to `/workspaces/{workspace_id}/members` with a body containing a `user_id` (any account on the platform) and `role: "owner"`. The `require_workspace_member` dependency passes because the caller is a member, and `MemberService.add` writes the new owner row without any caller-permission check. The attacker then switches to the newly-owner account and gains full workspace control. [ref_id=1] [ref_id=2]
Affected code
The `POST /workspaces/{workspace_id}/members` route in `src/praisonai-platform/praisonai_platform/api/routes/workspaces.py` (lines 92-101) uses `Depends(require_workspace_member)` with the default `min_role="member"`. The downstream `MemberService.add` in `src/praisonai-platform/praisonai_platform/services/member_service.py` (lines 26-38) validates only that the `role` string is in `VALID_ROLES` but never checks whether the caller has permission to assign that role. [ref_id=1] [ref_id=2]
What the fix does
The patch replaces `Depends(require_workspace_member)` with a new dependency that requires `min_role="owner"`, and adds an explicit check that only an existing owner can assign the `"owner"` role to another user. This ensures that a member-level token cannot elevate any account to owner. [ref_id=1] [ref_id=2]
Preconditions
- configThe praisonai-platform is deployed in a multi-tenant configuration
- authThe attacker possesses a valid member-level token for the target workspace
- inputThe attacker knows or can register a second user_id on the platform
Generated on Jun 1, 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.