Skip to main content

PraisonAI Platform CVE-2026-47418

HIGH
Authorization Bypass Through User-Controlled Key (CWE-639)
2026-06-01 https://github.com/MervinPraison/PraisonAI GHSA-943m-6wx2-rc2j
8.1
CVSS 3.1 · GitHub Advisory
Share

Severity by source

GitHub Advisory PRIMARY
8.1 HIGH
AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N

Primary rating from GitHub Advisory · only source for this CVE.

CVSS VectorGitHub Advisory

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
Low
User Interaction
None
Scope
Unchanged
Confidentiality
High
Integrity
High
Availability
None

Lifecycle Timeline

2
Source Code Evidence Fetched
Jun 01, 2026 - 14:51 vuln.today
Analysis Generated
Jun 01, 2026 - 14:51 vuln.today

DescriptionGitHub Advisory

Summary

Type: Insecure Direct Object Reference. The project CRUD endpoints (GET / PATCH / DELETE /workspaces/{workspace_id}/projects/{project_id} and GET .../{project_id}/stats) gate access on require_workspace_member(workspace_id) only, then resolve project_id through ProjectService.get(project_id) / update(project_id, ...) / delete(project_id) / get_stats(project_id). None of these calls thread workspace_id through to constrain the lookup. A user who is a member of any workspace W1 can read, modify, delete, or read stats for projects that belong to a different workspace W2. File: src/praisonai-platform/praisonai_platform/services/project_service.py, lines 47-108; route handlers at src/praisonai-platform/praisonai_platform/api/routes/projects.py, lines 51-108. Root cause: identical to the agent and issue IDORs in this codebase. The route accepts workspace_id from URL, uses it solely for the membership gate, then calls ProjectService.get(project_id) which is session.get(Project, project_id) - a primary-key-only lookup with no workspace_id predicate. update and delete call self.get(project_id) first, inheriting the gap. get_stats likewise has no workspace check.

Affected Code

File 1: src/praisonai-platform/praisonai_platform/services/project_service.py, lines 47-108.

python
class ProjectService:
    ...

    async def get(self, project_id: str) -> Optional[Project]:
        """Get project by ID."""
        return await self._session.get(Project, project_id)
# <-- BUG: no workspace_id predicate

    async def update(
        self,
        project_id: str,
        ...
    ) -> Optional[Project]:
        project = await self.get(project_id)
# <-- inherits the gap
        ...

    async def delete(self, project_id: str) -> bool:
        project = await self.get(project_id)
# <-- inherits the gap
        ...

    async def get_stats(self, project_id: str) -> dict:
        ...
# <-- also no workspace check; returns issue counts for any project

File 2: src/praisonai-platform/praisonai_platform/api/routes/projects.py, lines 51-108.

python
@router.get("/{project_id}", response_model=ProjectResponse)
async def get_project(
    workspace_id: str,
    project_id: str,
    user: AuthIdentity = Depends(require_workspace_member),
    session: AsyncSession = Depends(get_db),
):
    svc = ProjectService(session)
    project = await svc.get(project_id)
# <-- workspace_id never threaded through
    if project is None:
        raise HTTPException(status_code=404, detail="Project not found")
    return ProjectResponse.model_validate(project)


@router.patch("/{project_id}", response_model=ProjectResponse)
async def update_project(...):
    svc = ProjectService(session)
    project = await svc.update(project_id, title=body.title, ...)
# <-- writes to any project in the DB

@router.delete("/{project_id}", ...)
async def delete_project(...):
    deleted = await svc.delete(project_id)
# <-- deletes any project in the DB

@router.get("/{project_id}/stats")
async def project_stats(...):
    return await svc.get_stats(project_id)
# <-- returns stats for any project in the DB

Why it's wrong: workspace_id from the route is treated as a UI hint (gates "are you in some workspace W?") rather than an authoritative predicate (should also gate "is the project you are addressing actually inside W?"). The MemberService in this same codebase uses a composite (workspace_id, user_id) key and demonstrates the safe pattern; the project service simply did not apply it.

Exploit Chain

  1. Attacker registers a workspace W_attacker (where they are a member) and harvests a target project UUID P_T. Project IDs leak through the activity feed (act_svc.log records entity_id), issue records (every issue carries project_id), webhook payloads, error messages, exported issue dumps, or operator screenshots. State: attacker holds P_T.
  2. Attacker authenticates and sends GET /workspaces/W_attacker/projects/P_T. require_workspace_member(W_attacker, attacker) passes. State: control flow enters get_project with workspace_id=W_attacker, project_id=P_T.
  3. ProjectService.get(P_T) runs session.get(Project, "P_T"), which is SELECT * FROM projects WHERE id = 'P_T' LIMIT 1 with no workspace_id filter. The row is returned: title, description (often the project's confidential roadmap), status, lead_type, lead_id, icon, created_at, workspace_id (the foreign workspace's UUID is itself disclosed). State: response body is the JSON-serialised foreign project.
  4. Attacker repeats with PATCH /workspaces/W_attacker/projects/P_T and {"title": "<reset>", "description": "<wiped>", "status": "archived"}. update_project calls svc.update(P_T, ...) and mutates the foreign row. State: target project is silently re-titled, re-described, and archived.
  5. Attacker calls DELETE /workspaces/W_attacker/projects/P_T to delete the foreign project entirely. State: target project is gone (every issue still referencing it now has a dangling project_id).
  6. Attacker calls GET /workspaces/W_attacker/projects/P_T/stats to read aggregate issue counts (open/closed/in-progress) for the foreign project - useful for competitive intelligence even when full-issue read is not possible.
  7. Final state: any attacker with one workspace-member token can enumerate, exfiltrate, rewrite, and delete every project in the multi-tenant deployment given the project UUIDs.

Security Impact

Severity: sec-high. CVSS: network attack, low complexity, low privileges, no user interaction, scope unchanged, high confidentiality (project content + cross-workspace metadata via the leaked workspace_id field), high integrity (arbitrary writes / deletes), no availability claim (issue rows survive parent-project deletion). Attacker capability: read, edit, archive, delete, and stats-fingerprint any project in the multi-tenant deployment given the project UUID. Beyond plain content disclosure, the response also includes workspace_id, allowing the attacker to map the deployment's workspace topology (which workspaces exist, which projects each owns). Preconditions: praisonai-platform is deployed multi-tenant; the attacker has any membership token; the target project's UUID is known or guessable. Differential: source-inspection-verified end-to-end. The asymmetry between ProjectService.get(project_id) (no workspace check) and MemberService.get(workspace_id, user_id) (composite key check) confirms the gap. With the suggested fix below, ProjectService.get(workspace_id, project_id) returns None for foreign-workspace projects and the route handler returns 404.

Suggested Fix

Same shape as the companion agent and issue advisories. Make the resource-lookup query include the workspace predicate; treat foreign-workspace rows as 404.

diff
--- a/src/praisonai-platform/praisonai_platform/services/project_service.py
+++ b/src/praisonai-platform/praisonai_platform/services/project_service.py
@@ -45,9 +45,12 @@ class ProjectService:
         await self._session.flush()
         return project

-    async def get(self, project_id: str) -> Optional[Project]:
-        """Get project by ID."""
-        return await self._session.get(Project, project_id)
+    async def get(self, workspace_id: str, project_id: str) -> Optional[Project]:
+        """Get project by ID, scoped to a workspace."""
+        stmt = select(Project).where(
+            Project.id == project_id, Project.workspace_id == workspace_id
+        )
+        return (await self._session.execute(stmt)).scalar_one_or_none()

     async def update(
         self,
+        workspace_id: str,
         project_id: str,
         ...
     ) -> Optional[Project]:
-        project = await self.get(project_id)
+        project = await self.get(workspace_id, project_id)

-    async def delete(self, project_id: str) -> bool:
+    async def delete(self, workspace_id: str, project_id: str) -> bool:
-        project = await self.get(project_id)
+        project = await self.get(workspace_id, project_id)

-    async def get_stats(self, project_id: str) -> dict:
+    async def get_stats(self, workspace_id: str, project_id: str) -> dict:
+
# Also constrain the underlying issue counts query by workspace_id.

Update the route handlers in routes/projects.py to thread workspace_id through every call. The same single-key-lookup pattern is filed separately for AgentService, IssueService, CommentService, and LabelService.

AnalysisAI

{workspace_id}/projects/{project_id} only verify membership in the URL-supplied workspace, then perform primary-key-only lookups against the Project table without scoping by workspace_id. No public exploit identified at time of analysis, and EPSS/KEV signals are not provided for this advisory.

Technical ContextAI

The affected component is praisonai-platform, a Python/FastAPI multi-tenant backend for the PraisonAI project (pkg:pip/praisonai-platform). The vulnerability sits in ProjectService (services/project_service.py lines 47-108) and the project route handlers (api/routes/projects.py lines 51-108), which use SQLAlchemy AsyncSession. The root cause is CWE-639 (Authorization Bypass Through User-Controlled Key): ProjectService.get/update/delete/get_stats accept only project_id and execute session.get(Project, project_id) - a primary-key-only lookup with no workspace_id predicate - even though the URL carries workspace_id and the route depends on require_workspace_member(workspace_id). The same codebase's MemberService correctly uses a composite (workspace_id, user_id) key, demonstrating the intended pattern that ProjectService failed to apply, and equivalent IDORs are reported in AgentService, IssueService, CommentService, and LabelService.

RemediationAI

Vendor-released patch: upgrade praisonai-platform to 0.1.4 or later, which is the fixed version called out in the GitHub Security Advisory at https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-943m-6wx2-rc2j. The fix threads workspace_id through ProjectService.get/update/delete/get_stats and constrains the SQLAlchemy query with both Project.id project_id and Project.workspace_id workspace_id, so foreign-workspace rows return None and the route returns 404. If immediate upgrade is not possible, operators can apply the diff from the advisory as a local hotfix, or as a temporary compensating control restrict the project routes (GET/PATCH/DELETE /workspaces/{workspace_id}/projects/{project_id} and /stats) at a reverse proxy or API gateway to trusted internal users only - accepting that this disables legitimate multi-tenant project access - and audit application logs for cross-workspace project_id usage. Because the advisory notes that the same IDOR pattern affects AgentService, IssueService, CommentService, and LabelService, operators should track and apply the companion advisories rather than treating this CVE in isolation.

More in Python

View all
CVE-2025-24016 CRITICAL POC
9.9 Feb 10

Wazuh SIEM platform versions 4.4.0 through 4.9.0 contain an unsafe deserialization vulnerability in the DistributedAPI t

CVE-2025-27520 CRITICAL POC
9.8 Apr 04

BentoML version 1.4.2 and earlier contains an unauthenticated remote code execution vulnerability through insecure deser

CVE-2025-2945 CRITICAL POC
9.9 Apr 03

pgAdmin 4 contains critical remote code execution vulnerabilities in the Query Tool download and Cloud Deployment endpoi

CVE-2013-5093 MEDIUM POC
6.8 Sep 27

The renderLocalView function in render/views.py in graphite-web in Graphite 0.9.5 through 0.9.10 uses the pickle Python

CVE-2025-32375 CRITICAL POC
9.8 Apr 09

BentoML is a Python library for building online serving systems optimized for AI apps and model inference. Rated critica

CVE-2014-0224 HIGH POC
7.4 Jun 05

OpenSSL before 0.9.8za, 1.0.0 before 1.0.0m, and 1.0.1 before 1.0.1h does not properly restrict processing of ChangeCiph

CVE-2024-21644 HIGH POC
7.5 Jan 08

pyLoad download manager version prior to 0.5.0b3.dev77 exposes the Flask SECRET_KEY through an unauthenticated endpoint.

CVE-2017-9462 HIGH POC
8.8 Jun 06

In Mercurial before 4.1.3, "hg serve --stdio" allows remote authenticated users to launch the Python debugger, and conse

CVE-2026-39987 CRITICAL POC
9.3 Apr 08

Unauthenticated remote code execution in Marimo ≤0.20.4 allows attackers to execute arbitrary system commands via the `/

CVE-2024-21645 MEDIUM POC
5.3 Jan 08

pyLoad is the free and open-source Download Manager written in pure Python. Rated medium severity (CVSS 5.3), this vulne

CVE-2026-33017 CRITICAL POC
9.3 Mar 17

Langflow (a visual LLM pipeline builder) contains a critical unauthenticated code execution vulnerability (CVE-2026-3301

CVE-2026-55255 HIGH POC
8.4 Jun 19

Cross-user flow execution in Langflow (< 1.9.1) lets any authenticated API-key holder run another user's flow by passing

Share

CVE-2026-47418 vulnerability details – vuln.today

This site uses cookies essential for authentication and security. No tracking or analytics cookies are used. Privacy Policy