GitHub Advisoryadvisory · PyPI

GHSA-xwq8-frcg-77q8 praisonai-platform: Issue endpoints accept any issue_id without workspace ownership check, cross-workspace read/update/delete IDOR

HIGH Published 2026-06-01 Modified 2026-06-01 Official page ↗
Aliases
CVE-2026-47415
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:L
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 issue CRUD endpoints (`GET / PATCH / DELETE /workspaces/{workspace_id}/issues/{issue_id}`) gate access on `require_workspace_member(workspace_id)` only, then resolve `issue_id` through `IssueService.get(issue_id)` which is a primary-key lookup with no workspace constraint. A user who is a member of any workspace `W1` can read, modify, or delete issues that belong to a different workspace `W2`. File: `src/praisonai-platform/praisonai_platform/services/issue_service.py`, lines 72-156; route handlers at `src/praisonai-platform/praisonai_platform/api/routes/issues.py`, lines 82-137. Root cause: the route extracts `workspace_id` from the URL path, uses it solely for the membership gate, then calls `IssueService.get(issue_id)` / `IssueService.update(issue_id, ...)` / `IssueService.delete(issue_id)` without re-checking which workspace the issue actually belongs to. `IssueService.get` runs a single-key lookup; `update` and `delete` call `self.get(issue_id)` first and then mutate the returned row, inheriting the same gap. The `MemberService` in this same codebase uses a composite `(workspace_id, user_id)` key, proving the author knows the safe pattern; it was simply not applied to the issue, agent, project, comment, or label services. Affected Code File 1: `src/praisonai-platform/praisonai_platform/services/issue_service.py`, lines 72-75 and 97-156. ```python class IssueService: ... async def get(self, issue_id: str) -> Optional[Issue]: """Get issue by ID.""" return await self._session.get(Issue, issue_id) # Optional[Issue]: issue = await self.get(issue_id) # bool: issue = await self.get(issue_id) # ` to `GET /workspaces/W_attacker/issues/I_T`. `require_workspace_member(W_attacker, attacker)` passes (attacker is a member of `W_attacker`). State: control flow enters `get_issue` with `workspace_id=W_attacker, issue_id=I_T`. 3. `IssueService.get(I_T)` runs `session.get(Issue, "I_T")`, which is `SELECT * FROM issues WHERE id = 'I_T' LIMIT 1` with no `workspace_id = 'W_attacker'` filter. The row is returned in full — including `title`, `description` (often confidential bug-report content, customer PII, embedded credentials, or internal roadmap data), `status`, `priority`, `assignee_id`, `created_by`, and `project_id`. State: response body is the JSON-serialised foreign issue. 4. Attacker repeats with `PATCH /workspaces/W_attacker/issues/I_T` and a body of `{"description": "", "status": "closed", "project_id": ""}`. `update_issue` calls `svc.update(I_T, ...)` which loads the target row and mutates the listed fields. State: the foreign workspace's issue is silently re-described, re-statused, and re-projected. 5. Attacker calls `DELETE /workspaces/W_attacker/issues/I_T` to destroy the target issue. `IssueService.delete` loads the row and calls `session.delete()`. State: target issue is gone from the foreign workspace. 6. Final state: any attacker with one workspace-member token can enumerate, exfiltrate, rewrite, and delete every issue in the multi-tenant deployment given the issue UUIDs (which leak through the side channels above). The `act_svc.log(workspace_id, "issue.updated", "issue", issue.id, ...)` call at line 118 records the event under `W_attacker` rather than `W_target`, so the foreign workspace's audit trail does not record the tampering — making detection harder. Security Impact Severity: sec-high. CVSS 8.1: network attack, low complexity, low privileges (any workspace member), no user interaction, scope unchanged, high confidentiality (full issue body including any embedded secrets), high integrity (arbitrary writes including project re-assignment), low availability (DELETE wipes target issues). Attacker capability: with one workspace-member token plus a harvested issue UUID, an attacker reads the target issue's `title`, `description`, `status`, `priority`, `assignee_id`, and `project_id`; rewrites any of those fields (silent edit, false closure, malicious re-assignment); re-projects the issue to an unrelated project to confuse triagers; or deletes the issue altogether to destroy evidence of customer reports. Preconditions: `praisonai-platform` is deployed multi-tenant; the attacker has any membership token; the target issue's UUID is known or guessable (UUIDs leak through activity feeds, comment threads, error messages, exported dumps, and operator screenshots). Differential: source-inspection-verified end-to-end. The asymmetry between `IssueService.get(issue_id)` (no workspace check) and `MemberService.get(workspace_id, user_id)` (composite key check) in the same codebase confirms the pattern. With the suggested fix below applied, `IssueService.get(workspace_id, issue_id)` returns `None` for foreign-workspace issues, the route handler returns 404, and the foreign data is indistinguishable from a missing record. Suggest…

Related

Alias · CVE (1)

  • CVE-2026-47415 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

References