CVE-2026-47415: praisonai-platform: IDOR exposes cross-workspace tenant data

GHSA-xwq8-frcg-77q8 HIGH
Published June 1, 2026
CISO Take

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.

Sources: GitHub Advisory NVD ATLAS

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

Initial Access
Attacker creates a legitimate account and joins a low-privilege workspace (W_attacker) on the multi-tenant PraisonAI deployment using valid credentials.
AML.T0012
UUID Reconnaissance
Attacker harvests target issue UUIDs from activity feeds, comment threads, exported issue dumps, or error messages visible within their own workspace — UUIDs are not secret and surface through multiple side channels.
AML.T0000
IDOR Exploitation
Attacker submits authenticated API requests to /workspaces/W_attacker/issues/{victim_uuid}; the membership gate passes for W_attacker while the unconstrained database lookup returns the victim workspace's full issue record.
AML.T0049
Data Exfiltration and Tampering
Attacker reads confidential issue content including embedded credentials and PII, silently rewrites or deletes victim issues, and evades detection because audit logs attribute all events to the attacker's workspace rather than the victim's.
AML.T0025

What systems are affected?

Package Ecosystem Vulnerable Range Patched
praisonai-platform pip < 0.1.4 0.1.4
1 dependents 86% patched ~0d to patch Full package profile →

Do you use praisonai-platform? You're affected.

Severity & Risk

CVSS 3.1
8.3 / 10
EPSS
N/A
Exploitation Status
No known exploitation
Sophistication
Trivial

Attack Surface

AV AC PR UI S C I A
AV Network
AC Low
PR Low
UI None
S Unchanged
C High
I High
A Low

What should I do?

5 steps
  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.

Classification

Compliance Impact

This CVE is relevant to:

EU AI Act
Article 9 - Risk management system for high-risk AI
ISO 42001
A.8.1 - AI system data governance and access control
NIST AI RMF
GOVERN-1.7 - Organizational policies for AI risk management
OWASP LLM Top 10
LLM02 - Sensitive Information Disclosure

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

agent frameworksmulti-tenant AI platformsAI task and issue management systems

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

EU AI Act: Article 9
ISO 42001: A.8.1
NIST AI RMF: GOVERN-1.7
OWASP LLM Top 10: LLM02

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.

CVSS Vector

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:L

Timeline

Published
June 1, 2026
Last Modified
June 1, 2026
First Seen
June 1, 2026

Related Vulnerabilities