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

praisonai-platform: IDOR in dependency endpoints allows cross-workspace issue linking, reading, and deletion due to missing ownership checks

CVE-2026-47406

Description

Summary

Type: Insecure Direct Object Reference. The dependency endpoints (POST/GET /workspaces/{workspace_id}/issues/{issue_id}/dependencies and DELETE .../dependencies/{dep_id}) gate access on require_workspace_member(workspace_id) only, then dispatch to DependencyService calls that take URL/body-supplied issue and dependency IDs without verifying any of them belong to the membership-checked workspace. Most damaging: create_dependency accepts body.depends_on_issue_id from the request body — that ID is checked against nothing — letting an attacker create a "blocks" or "related" link between any two issues anywhere in the database. File: src/praisonai-platform/praisonai_platform/api/routes/dependencies.py, lines 22-58; services/dependency_service.py, lines 26-65. Root cause: the same Depends(require_workspace_member) default-min-role pattern as the companion IDORs, plus a service layer (DependencyService) where every method takes raw IDs and queries them directly. create(issue_id, depends_on_issue_id, ...) writes a row with no workspace verification on either ID. list_for_issue(issue_id) returns dependencies in either direction. delete(dep_id) is a primary-key delete with no workspace predicate.

Affected

Code

File 1: src/praisonai-platform/praisonai_platform/api/routes/dependencies.py, lines 22-58.

@router.post("/", response_model=DependencyResponse, status_code=status.HTTP_201_CREATED)
async def create_dependency(
    workspace_id: str,
    issue_id: str,
    body: DependencyCreate,
    user: AuthIdentity = Depends(require_workspace_member),
    session: AsyncSession = Depends(get_db),
):
    svc = DependencyService(session)
    dep = await svc.create(issue_id, body.depends_on_issue_id, body.type)  # <-- BUG: neither id is workspace-checked
    return DependencyResponse.model_validate(dep)


@router.get("/", response_model=List[DependencyResponse])
async def list_dependencies(
    workspace_id: str,
    issue_id: str,
    user: AuthIdentity = Depends(require_workspace_member),
    session: AsyncSession = Depends(get_db),
):
    svc = DependencyService(session)
    deps = await svc.list_for_issue(issue_id)                              # <-- BUG: returns dependencies for any issue
    return [DependencyResponse.model_validate(d) for d in deps]


@router.delete("/{dep_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_dependency(
    workspace_id: str,
    issue_id: str,
    dep_id: str,
    user: AuthIdentity = Depends(require_workspace_member),
    session: AsyncSession = Depends(get_db),
):
    svc = DependencyService(session)
    deleted = await svc.delete(dep_id)                                     # <-- BUG: deletes any dependency by id
    if not deleted:
        raise HTTPException(status_code=404, detail="Dependency not found")

File 2: src/praisonai-platform/praisonai_platform/services/dependency_service.py, lines 26-65.

async def create(self, issue_id: str, depends_on_issue_id: str, dep_type: str = "blocks") -> IssueDependency:
    if dep_type not in VALID_TYPES:
        raise ValueError(...)
    dep = IssueDependency(
        issue_id=issue_id,                                              # <-- accepts any
        depends_on_issue_id=depends_on_issue_id,                        # <-- accepts any (from request body)
        type=dep_type,
    )
    self._session.add(dep); await self._session.flush(); return dep

async def list_for_issue(self, issue_id: str) -> list[IssueDependency]:
    stmt = select(IssueDependency).where(
        (IssueDependency.issue_id == issue_id) | (IssueDependency.depends_on_issue_id == issue_id)
    )
    return list((await self._session.execute(stmt)).scalars().all())

async def delete(self, dep_id: str) -> bool:
    dep = await self.get(dep_id)                                        # session.get(IssueDependency, dep_id) — no workspace check
    ...

Why it's wrong: the request-body depends_on_issue_id is the worst part: an attacker can link any two issues across any two workspaces, polluting both workspaces' dependency graphs with attacker-chosen relationships ("blocks", "blocked_by", "related"). The triagers in the foreign workspace see their issue suddenly blocked by an unrelated foreign issue, breaking sprint planning and creating false correlation. The delete(dep_id) path lets an attacker remove legitimate cross-issue links between any two foreign workspaces, also disrupting their planning. The list_for_issue path leaks the dependency graph for any issue in the deployment.

Exploit

Chain

  1. Attacker is a member of workspace W_attacker and harvests two foreign-workspace issue UUIDs I1 (in W_target1) and I2 (in W_target2). They leak via the activity feed, comment threads, error messages, exported dumps, the agent prompt history, or any other channel that ever serialises an issue ID. State: attacker holds two foreign issue UUIDs.
  2. Attacker sends POST /workspaces/W_attacker/issues/I1/dependencies with Authorization: Bearer <attacker_jwt> and body {"depends_on_issue_id": "I2", "type": "blocks"}. State: control flow enters create_dependency with issue_id=I1 (foreign), depends_on_issue_id=I2 (foreign).
  3. require_workspace_member(W_attacker, attacker) passes (attacker is a member of W_attacker). DependencyService.create(I1, I2, "blocks") writes a new row IssueDependency(issue_id=I1, depends_on_issue_id=I2, type="blocks"). State: there is now a cross-workspace dependency between two foreign issues, written by the attacker.
  4. The triage UIs of W_target1 and W_target2 now show that the foreign issue is blocked by an unrelated issue in another workspace. Workflow rules that key off "cannot close while blocked" will refuse to let the legitimate triagers close I1. State: foreign workflow disrupted.
  5. Attacker repeats with GET /workspaces/W_attacker/issues/I1/dependencies to read the dependency graph for any foreign issue (information disclosure, project relationship mapping), or with DELETE .../{dep_id} (after enumerating dep_ids via the list call) to strip legitimate dependencies between foreign issues, breaking blocked-by chains.
  6. Final state: with one workspace-member token, the attacker reads, writes, and deletes dependencies on every issue in the multi-tenant deployment, polluting the dependency graphs of foreign workspaces.

Security

Impact

Severity: sec-high. CVSS 7.6: network attack, low complexity, low privileges, no user interaction, scope unchanged, high confidentiality (cross-workspace dependency graph disclosure), high integrity (cross-workspace dependency injection and deletion), no availability claim (workflow disruption is integrity, not availability). Attacker capability: read any issue's dependency graph; create arbitrary "blocks" / "blocked_by" / "related" links between any two issues across any two workspaces; delete any dependency by id. The most surprising primitive is the cross-workspace LINKING — the only one of the IDORs in this codebase where a single attacker request can affect TWO foreign workspaces at once. Preconditions: praisonai-platform is deployed multi-tenant; attacker has any membership token; foreign issue UUIDs are reachable. Differential: source-inspection-verified end-to-end. The asymmetry between this service (no workspace predicate anywhere) and MemberService.get(workspace_id, user_id) (correctly composite-keyed) confirms the gap. With the suggested fix below, the route would resolve both the URL issue_id and the body depends_on_issue_id against IssueService.get(workspace_id, ...) before allowing the dependency to be written.

Suggested

Fix

Resolve every issue id (URL and body) against workspace_id at the route layer before dispatching. The route helper from the issue-IDOR companion advisory can be reused.

--- a/src/praisonai-platform/praisonai_platform/api/routes/dependencies.py
+++ b/src/praisonai-platform/praisonai_platform/api/routes/dependencies.py
@@ -22,11 +22,16 @@
 @router.post("/", response_model=DependencyResponse, status_code=status.HTTP_201_CREATED)
 async def create_dependency(
     workspace_id: str,
     issue_id: str,
     body: DependencyCreate,
     user: AuthIdentity = Depends(require_workspace_member),
     session: AsyncSession = Depends(get_db),
 ):
+    issue_svc = IssueService(session)
+    if await issue_svc.get(workspace_id, issue_id) is None:
+        raise HTTPException(status_code=404, detail="Issue not found")
+    if await issue_svc.get(workspace_id, body.depends_on_issue_id) is None:
+        raise HTTPException(status_code=404, detail="depends_on_issue_id not found in this workspace")
     svc = DependencyService(session)
     dep = await svc.create(issue_id, body.depends_on_issue_id, body.type)
     return DependencyResponse.model_validate(dep)

Apply the same issue_svc.get(workspace_id, issue_id) precondition to list_dependencies and delete_dependency (verifying both the issue and the dependency belong to workspace_id).

AI Insight

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

Insecure Direct Object Reference in PraisonAI platform dependency endpoints allows cross-workspace issue linking and unauthorized access.

Vulnerability

The PraisonAI platform contains an Insecure Direct Object Reference (IDOR) vulnerability in its dependency management endpoints. The routes POST/GET /workspaces/{workspace_id}/issues/{issue_id}/dependencies and DELETE .../dependencies/{dep_id} gate access using require_workspace_member(workspace_id) but fail to verify that the supplied issue_id or dep_id actually belong to the checked workspace. The create_dependency endpoint accepts body.depends_on_issue_id from the request body without any workspace validation, allowing an attacker to create a "blocks" or "related" link between any two issues in the database. The affected code is in src/praisonai-platform/praisonai_platform/api/routes/dependencies.py (lines 22-58) and services/dependency_service.py (lines 26-65). All versions prior to the security patch are affected [1][2].

Exploitation

An attacker must be a member of at least one workspace (to pass the require_workspace_member check). They can then send crafted HTTP requests to the dependency endpoints. For example, a POST to /workspaces/{attacker_workspace}/issues/{victim_issue_id}/dependencies with a JSON body containing {"depends_on_issue_id": "another_workspace_issue_id", "type": "blocks"} will create a dependency linking issues across workspaces. Similarly, a GET request with an arbitrary issue_id will list all dependencies of that issue, and a DELETE request with a known dep_id will delete it, regardless of workspace boundaries [1][2].

Impact

Successful exploitation allows an attacker to create, read, and delete dependencies between any issues in the database, regardless of workspace membership. This leads to data integrity violations (linking unrelated issues), information disclosure (exposing dependency relationships of issues in other workspaces), and potential denial of service (deleting critical dependencies). The attacker does not gain elevated privileges beyond their existing workspace membership, but the cross-workspace access violates the intended isolation model [1][2].

Mitigation

Not yet disclosed in the available references. The vendor has acknowledged the vulnerability and a fix is expected. As a temporary workaround, administrators should restrict workspace membership to trusted users and monitor for unusual dependency creation or deletion activity. Applying the upcoming security patch from the PraisonAI repository is recommended [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 dependency endpoints only verify workspace membership for the URL workspace_id but never verify that the issue_id or depends_on_issue_id parameters belong to that workspace, allowing an attacker to read, create, or delete dependencies on any issue across any workspace."

Attack vector

An attacker who is a member of any workspace can harvest foreign issue UUIDs (e.g. from activity feeds, comments, or error messages) and send a POST to `/workspaces/{attacker_workspace}/issues/{foreign_issue_1}/dependencies` with a JSON body containing a second foreign issue ID as `depends_on_issue_id`. The `require_workspace_member` check passes because the attacker belongs to the URL workspace, but the service layer creates a cross-workspace dependency between the two foreign issues without any authorization check [ref_id=1][ref_id=2]. The same pattern allows reading any issue's dependency graph via GET and deleting any dependency via DELETE.

Affected code

The vulnerability is in `src/praisonai-platform/praisonai_platform/api/routes/dependencies.py` (lines 22–58) and `src/praisonai-platform/praisonai_platform/services/dependency_service.py` (lines 26–65). The three route handlers (`create_dependency`, `list_dependencies`, `delete_dependency`) gate access only on `require_workspace_member(workspace_id)` and then pass raw issue/dependency IDs to `DependencyService` methods that perform no workspace ownership check. `DependencyService.create()` writes a row with `issue_id` and `depends_on_issue_id` without verifying either belongs to the caller's workspace; `list_for_issue()` returns dependencies for any issue; `delete()` removes a dependency by primary key with no workspace predicate.

What the fix does

The patch provided in the advisory (not present in the supplied patch_id=3131084, which only bumps version numbers and fixes a logging import) suggests adding `IssueService.get(workspace_id, issue_id)` checks at the route layer for both the URL `issue_id` and the body `depends_on_issue_id` before dispatching to `DependencyService`. This would ensure both issues belong to the workspace the caller is authorized for, preventing cross-workspace linking, reading, and deletion. The same workspace-ownership precondition should be applied to `list_dependencies` and `delete_dependency`.

Preconditions

  • configThe praisonai-platform must be deployed in a multi-tenant configuration with multiple workspaces.
  • authThe attacker must possess a valid membership token for at least one workspace.
  • inputThe attacker must know or be able to discover foreign issue UUIDs (e.g. via activity feeds, comments, error messages, or agent prompt history).

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.