CVE-2026-48169
PraisonAI has Cross-Workspace IDOR and Privilege Escalation via Platform API
PraisonAI is a multi-agent teams system. Versions prior to 0.1.4 of the PraisonAI Platform API have two authorization failures that together break workspace isolation. The service layer for issues and projects performs global primary-key lookups without checking workspace ownership, so any authenticated user can read, modify, and delete resources in any workspace just by swapping UUIDs in their API requests. On top of that, every member management endpoint (add, update role, remove) only requires min_role="member", which lets any workspace member promote themselves to owner and kick out the original owner. A low-privilege member of one workspace can steal data from every other workspace and take over any workspace they belong to. Both issues come from the same gap: the route layer pulls workspace_id from the URL and verifies membership, but the service layer ignores the workspace scope for resource lookups and ignores the caller's role level for member operations. The require_workspace_member() dependency does its job correctly. The problem is that the service layer doesn't use the information it provides. Version 0.1.4 of the PraisonAI Platform API patch 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
- High
Exploit likelihood
What each source reports
CVE List Score used
8.8
PraisonAI has Cross-Workspace IDOR and Privilege Escalation via Platform API
PraisonAI is a multi-agent teams system. Versions prior to 0.1.4 of the PraisonAI Platform API have two authorization failures that together break workspace isolation. The service layer for issues and projects performs global primary-key lookups without checking workspace ownership, so any authenticated user can read, modify, and delete resources in any workspace just by swapping UUIDs in their API requests. On top of that, every member management endpoint (add, update role, remove) only requires min_role="member", which lets any workspace member promote themselves to owner and kick out the original owner. A low-privilege member of one workspace can steal data from every other workspace and take over any workspace they belong to. Both issues come from the same gap: the route layer pulls workspace_id from the URL and verifies membership, but the service layer ignores the workspace scope for resource lookups and ignores the caller's role level for member operations. The require_workspace_member() dependency does its job correctly. The problem is that the service layer doesn't use the information it provides. Version 0.1.4 of the PraisonAI Platform API patch the issue.
- Record
CVE-2026-48169- CVSS 3.1
- 8.8 High
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H - Weakness
- CWE-639, CWE-862
- Updated
- 2026-11-02
EUVDEUVD-2026-54685
8.8
PraisonAI is a multi-agent teams system. Versions prior to 0.1.4 of the PraisonAI Platform API have two authorization failures that together break workspace isolation. The service layer for issues and projects performs global primary-key lookups without checking workspace ownership, so any authenticated user can read, modify, and delete resources in any workspace just by swapping UUIDs in their API requests. On top of that, every member management endpoint (add, update role, remove) only requires min_role="member", which lets any workspace member promote themselves to owner and kick out the original owner. A low-privilege member of one workspace can steal data from every other workspace and take over any workspace they belong to. Both issues come from the same gap: the route layer pulls workspace_id from the URL and verifies membership, but the service layer ignores the workspace scope for resource lookups and ignores the caller's role level for member operations. The require_workspace_member() dependency does its job correctly. The problem is that the service layer doesn't use the information it provides. Version 0.1.4 of the PraisonAI Platform API patch the issue.
- Record
EUVD-2026-54685- CVSS 3.1
- 8.8 High
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H - Updated
- 2026-11-02
GitHub AdvisoriesGHSA-gv23-xrm3-8c62
8.8
PraisonAI has Cross-Workspace IDOR and Privilege Escalation via Platform API
Summary
The PraisonAI Platform API has two authorization failures that together break workspace isolation. The service layer for issues and projects performs global primary-key lookups without checking workspace ownership, so any authenticated user can read, modify, and delete resources in any workspace just by swapping UUIDs in their API requests. On top of that, every member management endpoint (add, update role, remove) only requires min_role="member", which lets any workspace member promote themselves to owner and kick out the original owner. A low-privilege member of one workspace can steal data from every other workspace and take over any workspace they belong to.
Both issues come from the same gap: the route layer pulls workspace_id from the URL and verifies membership, but the service layer ignores the workspace scope for resource lookups and ignores the caller's role level for member operations. The require_workspace_member() dependency does its job correctly. The problem is that the service layer doesn't use the information it provides.
Details
Part 1: Cross-Workspace IDOR (Issues and Projects)
Vulnerable Files:
praisonai_platform/services/issue_service.pypraisonai_platform/services/project_service.pypraisonai_platform/api/routes/issues.pypraisonai_platform/api/routes/projects.py
There is a consistent split between the route layer and the service layer. Routes pull workspace_id from the URL and verify membership:
GET /api/v1/workspaces/{workspace_id}/issues/{issue_id}
^^^^^^^^^^^^^^
require_workspace_member() checks this
But the service methods these routes call perform global lookups that ignore workspace_id entirely:
IssueService.get(), line 72:
async def get(self, issue_id: str) -> Optional[Issue]:
"""Get issue by ID."""
return await self._session.get(Issue, issue_id)
ProjectService.get(), line 47:
async def get(self, project_id: str) -> Optional[Project]:
"""Get project by ID."""
return await self._session.get(Project, project_id)
Both use session.get(Model, pk), which is a global lookup by primary key with no WHERE workspace_id = ? filter.
Compare that with the properly scoped list_for_workspace() methods in the same files:
IssueService.list_for_workspace(), line 76:
async def list_for_workspace(self, workspace_id: str, ...) -> list[Issue]:
stmt = select(Issue).where(Issue.workspace_id == workspace_id)
# ... properly scoped
The listing is scoped correctly. The get, update, and delete methods are not. Since update() and delete() in both services call self.get() internally, the workspace bypass cascades through all write operations too.
Route that discards workspace_id, issues.py line 82:
@router.get("/{issue_id}", response_model=IssueResponse)
async def get_issue(
workspace_id: str, # Extracted from URL
issue_id: str,
user: AuthIdentity = Depends(require_workspace_member), # Membership verified
session: AsyncSession = Depends(get_db),
):
svc = IssueService(session)
issue = await svc.get(issue_id) # workspace_id never passed to service
All affected operations:
| Service | Method | Line | Workspace scoped? |
|---------|--------|------|-------------------|
| IssueService | get() | 72 | No, uses session.get(Issue, issue_id) |
| IssueService | update() | 97 | No, calls self.get(issue_id) |
| IssueService | delete() | 150 | No, calls self.get(issue_id) |
| IssueService | list_for_workspace() | 76 | Yes, filters by workspace_id |
| ProjectService | get() | 47 | No, uses session.get(Project, project_id) |
| ProjectService | update() | 62 | No, calls self.get(project_id) |
| ProjectService | delete() | 88 | No, calls self.get(project_id) |
| ProjectService | get_stats() | 97 | No, only…
- Record
GHSA-gv23-xrm3-8c62- CVSS 3.1
- 8.8 High
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H - Weakness
- CWE-639, CWE-862
- Updated
- 2026-11-02
NVD
8.8
PraisonAI is a multi-agent teams system. Versions prior to 0.1.4 of the PraisonAI Platform API have two authorization failures that together break workspace isolation. The service layer for issues and projects performs global primary-key lookups without checking workspace ownership, so any authenticated user can read, modify, and delete resources in any workspace just by swapping UUIDs in their API requests. On top of that, every member management endpoint (add, update role, remove) only requires min_role="member", which lets any workspace member promote themselves to owner and kick out the original owner. A low-privilege member of one workspace can steal data from every other workspace and take over any workspace they belong to. Both issues come from the same gap: the route layer pulls workspace_id from the URL and verifies membership, but the service layer ignores the workspace scope for resource lookups and ignores the caller's role level for member operations. The require_workspace_member() dependency does its job correctly. The problem is that the service layer doesn't use the information it provides. Version 0.1.4 of the PraisonAI Platform API patch the issue.
- Record
CVE-2026-48169- CVSS 3.1
- 8.8 High
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H - Weakness
- CWE-639, CWE-862
- Updated
- 2026-11-02
OSV
8.8
PraisonAI has Cross-Workspace IDOR and Privilege Escalation via Platform API
PraisonAI is a multi-agent teams system. Versions prior to 0.1.4 of the PraisonAI Platform API have two authorization failures that together break workspace isolation. The service layer for issues and projects performs global primary-key lookups without checking workspace ownership, so any authenticated user can read, modify, and delete resources in any workspace just by swapping UUIDs in their API requests. On top of that, every member management endpoint (add, update role, remove) only requires min_role="member", which lets any workspace member promote themselves to owner and kick out the original owner. A low-privilege member of one workspace can steal data from every other workspace and take over any workspace they belong to. Both issues come from the same gap: the route layer pulls workspace_id from the URL and verifies membership, but the service layer ignores the workspace scope for resource lookups and ignores the caller's role level for member operations. The require_workspace_member() dependency does its job correctly. The problem is that the service layer doesn't use the information it provides. Version 0.1.4 of the PraisonAI Platform API patch the issue.
- Record
CVE-2026-48169- CVSS 3.1
- 8.8 High
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H - Weakness
- CWE-639, CWE-862
- Updated
- 2026-11-02
OSVGHSA-gv23-xrm3-8c62
8.8
PraisonAI has Cross-Workspace IDOR and Privilege Escalation via Platform API
Summary
The PraisonAI Platform API has two authorization failures that together break workspace isolation. The service layer for issues and projects performs global primary-key lookups without checking workspace ownership, so any authenticated user can read, modify, and delete resources in any workspace just by swapping UUIDs in their API requests. On top of that, every member management endpoint (add, update role, remove) only requires min_role="member", which lets any workspace member promote themselves to owner and kick out the original owner. A low-privilege member of one workspace can steal data from every other workspace and take over any workspace they belong to.
Both issues come from the same gap: the route layer pulls workspace_id from the URL and verifies membership, but the service layer ignores the workspace scope for resource lookups and ignores the caller's role level for member operations. The require_workspace_member() dependency does its job correctly. The problem is that the service layer doesn't use the information it provides.
Details
Part 1: Cross-Workspace IDOR (Issues and Projects)
Vulnerable Files:
praisonai_platform/services/issue_service.pypraisonai_platform/services/project_service.pypraisonai_platform/api/routes/issues.pypraisonai_platform/api/routes/projects.py
There is a consistent split between the route layer and the service layer. Routes pull workspace_id from the URL and verify membership:
GET /api/v1/workspaces/{workspace_id}/issues/{issue_id}
^^^^^^^^^^^^^^
require_workspace_member() checks this
But the service methods these routes call perform global lookups that ignore workspace_id entirely:
IssueService.get(), line 72:
async def get(self, issue_id: str) -> Optional[Issue]:
"""Get issue by ID."""
return await self._session.get(Issue, issue_id)
ProjectService.get(), line 47:
async def get(self, project_id: str) -> Optional[Project]:
"""Get project by ID."""
return await self._session.get(Project, project_id)
Both use session.get(Model, pk), which is a global lookup by primary key with no WHERE workspace_id = ? filter.
Compare that with the properly scoped list_for_workspace() methods in the same files:
IssueService.list_for_workspace(), line 76:
async def list_for_workspace(self, workspace_id: str, ...) -> list[Issue]:
stmt = select(Issue).where(Issue.workspace_id == workspace_id)
# ... properly scoped
The listing is scoped correctly. The get, update, and delete methods are not. Since update() and delete() in both services call self.get() internally, the workspace bypass cascades through all write operations too.
Route that discards workspace_id, issues.py line 82:
@router.get("/{issue_id}", response_model=IssueResponse)
async def get_issue(
workspace_id: str, # Extracted from URL
issue_id: str,
user: AuthIdentity = Depends(require_workspace_member), # Membership verified
session: AsyncSession = Depends(get_db),
):
svc = IssueService(session)
issue = await svc.get(issue_id) # workspace_id never passed to service
All affected operations:
| Service | Method | Line | Workspace scoped? |
|---------|--------|------|-------------------|
| IssueService | get() | 72 | No, uses session.get(Issue, issue_id) |
| IssueService | update() | 97 | No, calls self.get(issue_id) |
| IssueService | delete() | 150 | No, calls self.get(issue_id) |
| IssueService | list_for_workspace() | 76 | Yes, filters by workspace_id |
| ProjectService | get() | 47 | No, uses session.get(Project, project_id) |
| ProjectService | update() | 62 | No, calls self.get(project_id) |
| ProjectService | delete() | 88 | No, calls self.get(project_id) |
| ProjectService | get_stats() | 97 | No, only…
- Record
GHSA-gv23-xrm3-8c62- CVSS 3.1
- 8.8 High
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H - Weakness
- CWE-639, CWE-862
- Updated
- 2026-11-02
OSVPYSEC-2026-2935
8.8
PraisonAI has Cross-Workspace IDOR and Privilege Escalation via Platform API
Summary
The PraisonAI Platform API has two authorization failures that together break workspace isolation. The service layer for issues and projects performs global primary-key lookups without checking workspace ownership, so any authenticated user can read, modify, and delete resources in any workspace just by swapping UUIDs in their API requests. On top of that, every member management endpoint (add, update role, remove) only requires min_role="member", which lets any workspace member promote themselves to owner and kick out the original owner. A low-privilege member of one workspace can steal data from every other workspace and take over any workspace they belong to.
Both issues come from the same gap: the route layer pulls workspace_id from the URL and verifies membership, but the service layer ignores the workspace scope for resource lookups and ignores the caller's role level for member operations. The require_workspace_member() dependency does its job correctly. The problem is that the service layer doesn't use the information it provides.
Details
Part 1: Cross-Workspace IDOR (Issues and Projects)
Vulnerable Files:
praisonai_platform/services/issue_service.pypraisonai_platform/services/project_service.pypraisonai_platform/api/routes/issues.pypraisonai_platform/api/routes/projects.py
There is a consistent split between the route layer and the service layer. Routes pull workspace_id from the URL and verify membership:
GET /api/v1/workspaces/{workspace_id}/issues/{issue_id}
^^^^^^^^^^^^^^
require_workspace_member() checks this
But the service methods these routes call perform global lookups that ignore workspace_id entirely:
IssueService.get(), line 72:
async def get(self, issue_id: str) -> Optional[Issue]:
"""Get issue by ID."""
return await self._session.get(Issue, issue_id)
ProjectService.get(), line 47:
async def get(self, project_id: str) -> Optional[Project]:
"""Get project by ID."""
return await self._session.get(Project, project_id)
Both use session.get(Model, pk), which is a global lookup by primary key with no WHERE workspace_id = ? filter.
Compare that with the properly scoped list_for_workspace() methods in the same files:
IssueService.list_for_workspace(), line 76:
async def list_for_workspace(self, workspace_id: str, ...) -> list[Issue]:
stmt = select(Issue).where(Issue.workspace_id == workspace_id)
# ... properly scoped
The listing is scoped correctly. The get, update, and delete methods are not. Since update() and delete() in both services call self.get() internally, the workspace bypass cascades through all write operations too.
Route that discards workspace_id, issues.py line 82:
@router.get("/{issue_id}", response_model=IssueResponse)
async def get_issue(
workspace_id: str, # Extracted from URL
issue_id: str,
user: AuthIdentity = Depends(require_workspace_member), # Membership verified
session: AsyncSession = Depends(get_db),
):
svc = IssueService(session)
issue = await svc.get(issue_id) # workspace_id never passed to service
All affected operations:
| Service | Method | Line | Workspace scoped? |
|---------|--------|------|-------------------|
| IssueService | get() | 72 | No, uses session.get(Issue, issue_id) |
| IssueService | update() | 97 | No, calls self.get(issue_id) |
| IssueService | delete() | 150 | No, calls self.get(issue_id) |
| IssueService | list_for_workspace() | 76 | Yes, filters by workspace_id |
| ProjectService | get() | 47 | No, uses session.get(Project, project_id) |
| ProjectService | update() | 62 | No, calls self.get(project_id) |
| ProjectService | delete() | 88 | No, calls self.get(project_id) |
| ProjectService | get_stats() | 97 | No, only…
- Record
PYSEC-2026-2935- CVSS 3.1
- 8.8 High
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H - Updated
- 2026-11-02
Affected products
Affected packages
| Package | Affected | Fixed |
|---|---|---|
praisonai-platformPyPI |
through 0.1.2 | 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 |
|---|---|---|
| first indexed | CVE List, EUVD, GitHub Advisories, NVD, OSV |
Dates are when Every CVE recorded each change. Upstream dates may differ.
In short
CVE-2026-48169 is a high vulnerability (CVSS 8.8) in Mervinpraison Praisonai-platform, published May 29, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.44%, the 38th percentile. Fixed in praisonai-platform 0.1.4.
References
github.com/MervinPraison/PraisonAI/security/advisories/GHSA-gv23-xrm3-8c62
github.com/pypa/advisory-database/tree/main/vulns/praisonai-platform/PYSEC-2026-2935.yaml
nvd.nist.gov/vuln/detail/CVE-2026-48169
github.com/advisories/GHSA-gv23-xrm3-8c62
github.com/CVEProject/cvelistV5/tree/main/cves/2026/48xxx/CVE-2026-48169.json
github.com/MervinPraison/PraisonAI
pypi.org/project/praisonai-platform
Left out here: the rest of 3 source descriptions. The JSON record has them.