Every CVE Search/ Get API key

CVE-2026-48169

PraisonAI has Cross-Workspace IDOR and Privilege Escalation via Platform API

8.8 HighFix available

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

0.44%Up 0.4 points since Jun 12, 2026
90%50%10%1%

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 HighCVSS: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 HighCVSS: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.py
  • praisonai_platform/services/project_service.py
  • praisonai_platform/api/routes/issues.py
  • praisonai_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 HighCVSS: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 HighCVSS: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 HighCVSS: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.py
  • praisonai_platform/services/project_service.py
  • praisonai_platform/api/routes/issues.py
  • praisonai_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 HighCVSS: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.py
  • praisonai_platform/services/project_service.py
  • praisonai_platform/api/routes/issues.py
  • praisonai_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 HighCVSS: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

Affected packages
PackageAffectedFixed
praisonai-platform
PyPI
through 0.1.2
0.1.4
praisonai-platform
PyPI
before 0.1.4, plus listed versions
0.1.00.1.10.1.20.1.3
0.1.4

History

History
RecordedChangedSources
first indexedCVE 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

This page doesn't list everything

Left out here: the rest of 3 source descriptions. The JSON record has them.

MoveEnter Open