GitHub Advisoryadvisory · PyPI
GHSA-943m-6wx2-rc2j praisonai-platform: Project endpoints accept any project_id without workspace ownership check, cross-workspace read/update/delete IDOR
HIGH
Official page ↗
- Aliases
- CVE-2026-47418
- Ecosystem
- PyPI
- Packages
- praisonai-platform
- Severity
- High
- CVSS vector
- CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N
- Weaknesses
- CWE-639
- Reviewed
- 2026-06-01
Affected packages
• PyPI praisonai-platform: < 0.1.4 → fixed in 0.1.4
Description
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) # Optional[Project]:
project = await self.get(project_id) # bool:
project = await self.get(project_id) # dict:
... # ", "description": "", "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]:
• pro…
Related
Alias · CVE (1)
- CVE-2026-47418 PraisonAI Platform is the platform layer for the PraisonAI multi-agent teams system. vulnerability
Weakness · CWE (1)
- CWE-639 Authorization Bypass Through User-Controlled Key weakness