CVE-2026-47415
praisonai-platform: Issue endpoints accept any issue_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 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. 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
- Low
Exploit likelihood
What each source reports
CVE List Score used
8.3
praisonai-platform: Issue endpoints accept any issue_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 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. PraisonAI Platform version 0.1.4 patches the issue.
- Record
CVE-2026-47415- CVSS 3.1
- 8.3 High
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:L - Weakness
- CWE-639
- Updated
- 2026-11-02
EUVDEUVD-2026-46333
8.3
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 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. PraisonAI Platform version 0.1.4 patches the issue.
- Record
EUVD-2026-46333- CVSS 3.1
- 8.3 High
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:L - Updated
- 2026-11-02
GitHub AdvisoriesGHSA-xwq8-frcg-77q8
8.3
praisonai-platform: Issue endpoints accept any issue_id without workspace ownership check, cross-workspace read/update/delete IDOR
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.
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.
@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,…
- Record
GHSA-xwq8-frcg-77q8- CVSS 3.1
- 8.3 High
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:L - Weakness
- CWE-639
- Updated
- 2026-11-02
NVD
8.3
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 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. PraisonAI Platform version 0.1.4 patches the issue.
- Record
CVE-2026-47415- CVSS 3.1
- 8.3 High
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:L - Weakness
- CWE-639
- Updated
- 2026-11-02
OSV
8.3
praisonai-platform: Issue endpoints accept any issue_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 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. PraisonAI Platform version 0.1.4 patches the issue.
- Record
CVE-2026-47415- CVSS 3.1
- 8.3 High
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:L - Weakness
- CWE-639
- Updated
- 2026-11-02
OSVGHSA-xwq8-frcg-77q8
8.3
praisonai-platform: Issue endpoints accept any issue_id without workspace ownership check, cross-workspace read/update/delete IDOR
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.
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.
@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,…
- Record
GHSA-xwq8-frcg-77q8- CVSS 3.1
- 8.3 High
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:L - Weakness
- CWE-639
- Updated
- 2026-11-02
OSVPYSEC-2026-2939
8.3
praisonai-platform: Issue endpoints accept any issue_id without workspace ownership check, cross-workspace read/update/delete IDOR
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.
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.
@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,…
- Record
PYSEC-2026-2939- CVSS 3.1
- 8.3 High
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:L - 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-47415 is a high vulnerability (CVSS 8.3) 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.39%, the 33rd percentile. Fixed in praisonai-platform 0.1.4.
References
github.com/MervinPraison/PraisonAI/security/advisories/GHSA-xwq8-frcg-77q8
github.com/MervinPraison/PraisonAI/commit/ef79b7a0561796ad9807f0f09538c25cc78d3619
github.com/advisories/GHSA-xwq8-frcg-77q8
github.com/CVEProject/cvelistV5/tree/main/cves/2026/47xxx/CVE-2026-47415.json
nvd.nist.gov/vuln/detail/CVE-2026-47415
github.com/MervinPraison/PraisonAI
pypi.org/project/praisonai-platform
Left out here: the rest of 3 source descriptions. The JSON record has them.