CVE-2026-47418
praisonai-platform: Project endpoints accept any project_id without workspace ownership check, cross-workspace read/update/delete IDOR
PraisonAI Platform is the platform layer for the PraisonAI multi-agent teams system. Versions prior to 0.1.4 have an 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. PraisonAI Platform version 0.1.4 patches the issue.
Attack profile
How easy it is to exploit
- Attack vector
- Network
- Complexity
- Low
- Privileges needed
- Low
- User interaction
- None
What an attacker can affect
- Confidentiality
- High
- Integrity
- High
- Availability
- None
Exploit likelihood
What each source reports
CVE List Score used
8.1
praisonai-platform: Project endpoints accept any project_id without workspace ownership check, cross-workspace read/update/delete IDOR
PraisonAI Platform is the platform layer for the PraisonAI multi-agent teams system. Versions prior to 0.1.4 have an 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. PraisonAI Platform version 0.1.4 patches the issue.
- Record
CVE-2026-47418- CVSS 3.1
- 8.1 High
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N - Weakness
- CWE-639
- Updated
- 2026-11-02
EUVDEUVD-2026-46337
8.1
PraisonAI Platform is the platform layer for the PraisonAI multi-agent teams system. Versions prior to 0.1.4 have an 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. PraisonAI Platform version 0.1.4 patches the issue.
- Record
EUVD-2026-46337- CVSS 3.1
- 8.1 High
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N - Updated
- 2026-11-02
GitHub AdvisoriesGHSA-943m-6wx2-rc2j
8.1
praisonai-platform: Project endpoints accept any project_id without workspace ownership check, cross-workspace read/update/delete IDOR
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.
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.
@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
- Attacker registers a workspace
W_attacker(where they are a member) and harvests a target project UUIDP_T. Project IDs leak through the activity feed (act_svc.logrecordsentity_id),…
- Record
GHSA-943m-6wx2-rc2j- CVSS 3.1
- 8.1 High
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N - Weakness
- CWE-639
- Updated
- 2026-11-02
NVD
8.1
PraisonAI Platform is the platform layer for the PraisonAI multi-agent teams system. Versions prior to 0.1.4 have an 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. PraisonAI Platform version 0.1.4 patches the issue.
- Record
CVE-2026-47418- CVSS 3.1
- 8.1 High
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N - Weakness
- CWE-639
- Updated
- 2026-11-02
OSV
8.1
praisonai-platform: Project endpoints accept any project_id without workspace ownership check, cross-workspace read/update/delete IDOR
PraisonAI Platform is the platform layer for the PraisonAI multi-agent teams system. Versions prior to 0.1.4 have an 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. PraisonAI Platform version 0.1.4 patches the issue.
- Record
CVE-2026-47418- CVSS 3.1
- 8.1 High
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N - Weakness
- CWE-639
- Updated
- 2026-11-02
OSVGHSA-943m-6wx2-rc2j
8.1
praisonai-platform: Project endpoints accept any project_id without workspace ownership check, cross-workspace read/update/delete IDOR
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.
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.
@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
- Attacker registers a workspace
W_attacker(where they are a member) and harvests a target project UUIDP_T. Project IDs leak through the activity feed (act_svc.logrecordsentity_id),…
- Record
GHSA-943m-6wx2-rc2j- CVSS 3.1
- 8.1 High
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N - Weakness
- CWE-639
- Updated
- 2026-11-02
OSVPYSEC-2026-2932
8.1
praisonai-platform: Project endpoints accept any project_id without workspace ownership check, cross-workspace read/update/delete IDOR
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.
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.
@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
- Attacker registers a workspace
W_attacker(where they are a member) and harvests a target project UUIDP_T. Project IDs leak through the activity feed (act_svc.logrecordsentity_id),…
- Record
PYSEC-2026-2932- CVSS 3.1
- 8.1 High
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N - Updated
- 2026-11-02
Affected products
Affected packages
| Package | Affected | Fixed |
|---|---|---|
praisonai-platformPyPI |
before 0.1.4 | 0.1.4 |
praisonai-platformPyPI |
before 0.1.4, plus listed versions 0.1.00.1.10.1.20.1.3 |
0.1.4 |
History
| Recorded | Changed | Sources |
|---|---|---|
| aliases | EUVD | |
| first indexed | CVE List, GitHub Advisories, NVD, OSV |
Dates are when Every CVE recorded each change. Upstream dates may differ.
In short
CVE-2026-47418 is a high vulnerability (CVSS 8.1) in Mervinpraison Praisonai-platform, published June 1, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.41%, the 35th percentile. Fixed in praisonai-platform 0.1.4.
References
github.com/MervinPraison/PraisonAI/security/advisories/GHSA-943m-6wx2-rc2j
github.com/MervinPraison/PraisonAI/pull/1685
github.com/MervinPraison/PraisonAI/commit/ef79b7a0561796ad9807f0f09538c25cc78d3619
github.com/advisories/GHSA-943m-6wx2-rc2j
github.com/CVEProject/cvelistV5/tree/main/cves/2026/47xxx/CVE-2026-47418.json
nvd.nist.gov/vuln/detail/CVE-2026-47418
github.com/MervinPraison/PraisonAI
pypi.org/project/praisonai-platform
Left out here: the rest of 3 source descriptions. The JSON record has them.