VYPR
Critical severity9.6NVD Advisory· Published Jun 1, 2026· Updated Jun 1, 2026

praisonai-platform: Any workspace member can add arbitrary user as owner via POST /workspaces/{id}/members

CVE-2026-47413

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

1

Patches

2
8c4ab718aeeb

PraisonAI Call API Release

https://github.com/MervinPraison/PraisonAIMervinPraisonOct 17, 2024Fixed in 0.1.4via ghsa-release-walk
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(&#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.3 gunicorn markdown\n&#34;)
    +            file.write(&#34;RUN pip install flask praisonai==0.1.4 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;)
                 
    
  • 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
    
8cffa23da4db

Praison AI Call basics

https://github.com/MervinPraison/PraisonAIMervinPraisonOct 17, 2024Fixed in 0.1.4via ghsa-release-walk
6 files changed · +1212 1162
  • docs/ui/ui.md+4 0 modified
    @@ -88,3 +88,7 @@ streamlit run app.py
     
     ### Manual Model Output
     ![Manual Model Output](../images/ui-step-11.png)
    +
    +## PraisonAI Call
    +
    +To use the PraisonAI Call feature:
    
  • poetry.lock+1162 1156 modified
  • praisonai/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

2

News mentions

0

No linked articles in our index yet.