CVE-2026-47415: praisonai-platform: IDOR exposes cross-workspace tenant data
GHSA-xwq8-frcg-77q8 HIGHpraisonai-platform before 0.1.4 contains an Insecure Direct Object Reference in its issue management API that allows any authenticated workspace member to read, modify, or delete issues belonging to entirely different tenants — the membership gate validates the attacker's own workspace but the underlying database lookup carries no workspace constraint, so substituting a victim's issue UUID in the request is sufficient to exfiltrate or tamper with it. The blast radius covers every multi-tenant PraisonAI deployment: issue records in AI agent platforms routinely contain confidential bug reports, customer PII, embedded credentials, and agent prompt content, and the 65 prior CVEs in this package signal a persistently underdeveloped security posture that warrants elevated scrutiny. No public exploit code exists and the CVE is not in CISA KEV, but the attack requires only trivial tooling — a valid JWT and a harvested UUID — with UUIDs leaking through activity feeds, comment threads, and exported dumps, placing exploitation well within reach of opportunistic actors. Patch to praisonai-platform 0.1.4 immediately, audit activity logs for cross-workspace issue access patterns, and note that the audit trail records events under the attacker's workspace rather than the victim's, making retrospective detection non-trivial without additional query-level logging.
What is the risk?
High risk in any multi-tenant deployment. Attack complexity is low, privileges required are minimal (any workspace member token), no user interaction is needed, and the vulnerability is remotely exploitable over the network. The write primitive is particularly dangerous: an attacker can not only exfiltrate issue content silently but also corrupt it (false closure, description wipe, project reassignment) and delete it entirely to destroy evidence. The asymmetry between the existing composite-key pattern in MemberService and the unconstrained single-key lookups across IssueService, AgentService, ProjectService, CommentService, and LabelService confirms this is a systemic authorization design gap rather than an isolated oversight. The 65 prior CVEs in the same package increase the probability that related IDOR variants exist in production deployments that have not yet received the 0.1.4 patch.
Attack Kill Chain
What systems are affected?
| Package | Ecosystem | Vulnerable Range | Patched |
|---|---|---|---|
| praisonai-platform | pip | < 0.1.4 | 0.1.4 |
Do you use praisonai-platform? You're affected.
Severity & Risk
Attack Surface
What should I do?
5 steps-
Patch: upgrade praisonai-platform to 0.1.4 which adds workspace_id as a query predicate to IssueService.get(), .update(), and .delete().
-
Verify scope: the advisory flags the same IDOR pattern in AgentService, ProjectService, CommentService, and LabelService — confirm 0.1.4 addresses all five or apply the composite-key fix (workspace_id AND resource_id) manually to each service.
-
Detection: query activity logs for issue_id values accessed outside their originating workspace_id; given that tamper events are recorded under the attacker's workspace rather than the victim's, cross-reference by issue_id across workspace boundaries.
-
Workaround (if patching is delayed): restrict API access to trusted networks or add a middleware layer that validates the resource's workspace_id matches the URL path parameter before any service method is called.
-
Audit UUIDs: review whether issue UUIDs are exposed in activity feeds, exports, or error messages and consider scoping those outputs to workspace-specific views to reduce reconnaissance surface.
Classification
Compliance Impact
This CVE is relevant to:
Frequently Asked Questions
What is CVE-2026-47415?
praisonai-platform before 0.1.4 contains an Insecure Direct Object Reference in its issue management API that allows any authenticated workspace member to read, modify, or delete issues belonging to entirely different tenants — the membership gate validates the attacker's own workspace but the underlying database lookup carries no workspace constraint, so substituting a victim's issue UUID in the request is sufficient to exfiltrate or tamper with it. The blast radius covers every multi-tenant PraisonAI deployment: issue records in AI agent platforms routinely contain confidential bug reports, customer PII, embedded credentials, and agent prompt content, and the 65 prior CVEs in this package signal a persistently underdeveloped security posture that warrants elevated scrutiny. No public exploit code exists and the CVE is not in CISA KEV, but the attack requires only trivial tooling — a valid JWT and a harvested UUID — with UUIDs leaking through activity feeds, comment threads, and exported dumps, placing exploitation well within reach of opportunistic actors. Patch to praisonai-platform 0.1.4 immediately, audit activity logs for cross-workspace issue access patterns, and note that the audit trail records events under the attacker's workspace rather than the victim's, making retrospective detection non-trivial without additional query-level logging.
Is CVE-2026-47415 actively exploited?
No confirmed active exploitation of CVE-2026-47415 has been reported, but organizations should still patch proactively.
How to fix CVE-2026-47415?
1. Patch: upgrade praisonai-platform to 0.1.4 which adds workspace_id as a query predicate to IssueService.get(), .update(), and .delete(). 2. Verify scope: the advisory flags the same IDOR pattern in AgentService, ProjectService, CommentService, and LabelService — confirm 0.1.4 addresses all five or apply the composite-key fix (workspace_id AND resource_id) manually to each service. 3. Detection: query activity logs for issue_id values accessed outside their originating workspace_id; given that tamper events are recorded under the attacker's workspace rather than the victim's, cross-reference by issue_id across workspace boundaries. 4. Workaround (if patching is delayed): restrict API access to trusted networks or add a middleware layer that validates the resource's workspace_id matches the URL path parameter before any service method is called. 5. Audit UUIDs: review whether issue UUIDs are exposed in activity feeds, exports, or error messages and consider scoping those outputs to workspace-specific views to reduce reconnaissance surface.
What systems are affected by CVE-2026-47415?
This vulnerability affects the following AI/ML architecture patterns: agent frameworks, multi-tenant AI platforms, AI task and issue management systems.
What is the CVSS score for CVE-2026-47415?
CVE-2026-47415 has a CVSS v3.1 base score of 8.3 (HIGH).
AI Security Impact
Affected AI Architectures
MITRE ATLAS Techniques
AML.T0012 Valid Accounts AML.T0025 Exfiltration via Cyber Means AML.T0036 Data from Information Repositories AML.T0049 Exploit Public-Facing Application AML.T0085 Data from AI Services Compliance Controls Affected
Technical Details
Original Advisory
## 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) # <-- BUG: no workspace_id predicate async def update( self, issue_id: str, title: Optional[str] = None, ... ) -> Optional[Issue]: issue = await self.get(issue_id) # <-- inherits the same gap if issue is None: return None ... return issue async def delete(self, issue_id: str) -> bool: issue = await self.get(issue_id) # <-- inherits the same gap if issue is None: return False await self._session.delete(issue) await self._session.flush() return True ``` **File 2:** `src/praisonai-platform/praisonai_platform/api/routes/issues.py`, lines 82-137. ```python @router.get("/{issue_id}", response_model=IssueResponse) async def get_issue( workspace_id: str, issue_id: str, user: AuthIdentity = Depends(require_workspace_member), # only checks membership in workspace_id session: AsyncSession = Depends(get_db), ): svc = IssueService(session) issue = await svc.get(issue_id) # <-- workspace_id never threaded through if issue is None: raise HTTPException(status_code=404, detail="Issue not found") return IssueResponse.model_validate(issue) @router.patch("/{issue_id}", response_model=IssueResponse) async def update_issue( workspace_id: str, issue_id: str, body: IssueUpdate, user: AuthIdentity = Depends(require_workspace_member), session: AsyncSession = Depends(get_db), ): svc = IssueService(session) issue = await svc.update( # <-- writes to any issue in the DB issue_id, title=body.title, description=body.description, status=body.status, priority=body.priority, assignee_type=body.assignee_type, assignee_id=body.assignee_id, project_id=body.project_id, ) ... ``` `delete_issue` (lines 127-137) repeats the pattern. **Why it's wrong:** `workspace_id` from the route is used solely as a membership predicate ("are you in some workspace W?"), never as a resource-ownership predicate ("is the issue you are addressing actually inside W?"). The standard FastAPI/SQLAlchemy fix is to make the resource-lookup query include the workspace constraint and treat absence as 404, so a foreign-workspace issue is indistinguishable from a non-existent one. The `update_issue` handler additionally allows the attacker to overwrite `project_id`, which can re-assign the foreign issue to an unrelated project the attacker also does not own — escalating the scope of the write primitive. ## Exploit Chain 1. Attacker registers a workspace `W_attacker` (where they are a member) and harvests a target issue UUID `I_T` from any side channel: the activity feed (`activity.py:log` records `issue_id=...`), comment threads, error messages, exported issue dumps, issue mentions in agent prompts, or operator screenshots. Issue IDs are uuid4 strings but they are not secret. State: attacker holds `I_T`. 2. Attacker authenticates and POSTs `Authorization: Bearer <attacker_jwt>` 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": "<reset>", "status": "closed", "project_id": "<arbitrary>"}`. `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. ## Suggested Fix Make every single-row resource lookup take the workspace predicate; treat foreign-workspace rows as 404. ```diff --- a/src/praisonai-platform/praisonai_platform/services/issue_service.py +++ b/src/praisonai-platform/praisonai_platform/services/issue_service.py @@ -69,9 +69,12 @@ class IssueService: await self._session.flush() return issue - async def get(self, issue_id: str) -> Optional[Issue]: - """Get issue by ID.""" - return await self._session.get(Issue, issue_id) + async def get(self, workspace_id: str, issue_id: str) -> Optional[Issue]: + """Get issue by ID, scoped to a workspace.""" + stmt = select(Issue).where( + Issue.id == issue_id, Issue.workspace_id == workspace_id + ) + return (await self._session.execute(stmt)).scalar_one_or_none() async def update( self, + workspace_id: str, issue_id: str, ... ) -> Optional[Issue]: - issue = await self.get(issue_id) + issue = await self.get(workspace_id, issue_id) ... - async def delete(self, issue_id: str) -> bool: + async def delete(self, workspace_id: str, issue_id: str) -> bool: - issue = await self.get(issue_id) + issue = await self.get(workspace_id, issue_id) ``` Update the route handlers in `routes/issues.py` to thread `workspace_id` through. The same pattern (single-key resource lookup gated only by workspace-member check) exists in `AgentService`, `ProjectService`, `CommentService`, and `LabelService`; each is a separate exploitable IDOR and should be filed as its own advisory so each gets a CVE.
Exploitation Scenario
An attacker operating a threat intelligence or red team engagement against a company using PraisonAI's hosted multi-tenant platform creates a free-tier account and becomes a member of their own workspace (W_attacker). They monitor the platform's activity feed and comment threads within W_attacker, where issue UUIDs from cross-workspace notification events occasionally surface — or they obtain a UUID via an exported issue dump from a shared project they were briefly invited to. Armed with a valid JWT and one victim issue UUID, they send GET /workspaces/W_attacker/issues/{victim_uuid} with their bearer token. The membership check passes for W_attacker, and IssueService.get() performs an unconstrained SELECT returning the full issue row from the victim workspace — including title, description, embedded credentials, assignee identity, and project linkage. They follow with PATCH requests to overwrite the issue's description and close it (suppressing security escalation), then DELETE to eliminate the record entirely. Throughout the attack, the audit trail records all events under W_attacker, leaving the victim workspace's incident log clean.
Weaknesses (CWE)
CVSS Vector
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:L References
Timeline
Related Vulnerabilities
CVE-2026-47392 9.9 praisonaiagents: RCE via Python sandbox bypass
Same package: praisonai GHSA-vc46-vw85-3wvm 9.8 PraisonAI: RCE via malicious workflow YAML execution
Same package: praisonai CVE-2026-39890 9.8 PraisonAI: YAML deserialization enables unauthenticated RCE
Same package: praisonai GHSA-9qhq-v63v-fv3j 9.8 PraisonAI: RCE via MCP command injection
Same package: praisonai CVE-2026-47410 9.8 praisonai-platform: hardcoded JWT → full account takeover
Same package: praisonai