CVE-2026-47407
PraisonAI Platform has a cross-workspace IDOR + member-role privilege escalation
PraisonAI Platform is the platform layer for the PraisonAI multi-agent teams system. Prior to version 0.1.4, the Platform server exposes resources under /api/v1/workspaces/{workspace_id}/... and protects them with a require_workspace_member(workspace_id) FastAPI dependency. The dependency only checks that the caller is a member of the workspace_id in the URL prefix. The route handlers then look up the inner resource (agent_id, issue_id, project_id, label_id, comment_id, dependency_id) by primary key alone. The resource's own workspace_id is never compared to the URL's workspace_id. A user can therefore put their own workspace in the URL prefix and any other workspace's resource ID in the path. The auth check passes, since they really are a member of the prefix workspace. The service then returns the cross-tenant resource for read, update, or delete. There is a second bug in the member-management routes (add_member, update_member_role, remove_member, update_workspace, delete_workspace). Each one inherits the default min_role="member" from require_workspace_member. Any basic member can therefore promote themselves to admin or owner, demote or remove other members, and delete the workspace. The role hierarchy exists in the schema but is not enforced. Registration is open at /api/v1/auth/register with no email verification. The default server bind is 0.0.0.0:8000 (python -m praisonai_platform). One curl from any unauthenticated network position is enough to bootstrap into the system. 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
- High
Exploit likelihood
What each source reports
CVE List Score used
9.4
PraisonAI Platform has a cross-workspace IDOR + member-role privilege escalation
PraisonAI Platform is the platform layer for the PraisonAI multi-agent teams system. Prior to version 0.1.4, the Platform server exposes resources under /api/v1/workspaces/{workspace_id}/... and protects them with a require_workspace_member(workspace_id) FastAPI dependency. The dependency only checks that the caller is a member of the workspace_id in the URL prefix. The route handlers then look up the inner resource (agent_id, issue_id, project_id, label_id, comment_id, dependency_id) by primary key alone. The resource's own workspace_id is never compared to the URL's workspace_id. A user can therefore put their own workspace in the URL prefix and any other workspace's resource ID in the path. The auth check passes, since they really are a member of the prefix workspace. The service then returns the cross-tenant resource for read, update, or delete. There is a second bug in the member-management routes (add_member, update_member_role, remove_member, update_workspace, delete_workspace). Each one inherits the default min_role="member" from require_workspace_member. Any basic member can therefore promote themselves to admin or owner, demote or remove other members, and delete the workspace. The role hierarchy exists in the schema but is not enforced. Registration is open at /api/v1/auth/register with no email verification. The default server bind is 0.0.0.0:8000 (python -m praisonai_platform). One curl from any unauthenticated network position is enough to bootstrap into the system. PraisonAI Platform version 0.1.4 patches the issue.
- Record
CVE-2026-47407- CVSS 4.0
- 9.4 Critical
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H - Weakness
- CWE-269, CWE-639, CWE-863
- Updated
- 2026-11-02
EUVDEUVD-2026-46319
9.4
PraisonAI Platform is the platform layer for the PraisonAI multi-agent teams system. Prior to version 0.1.4, the Platform server exposes resources under /api/v1/workspaces/{workspace_id}/... and protects them with a require_workspace_member(workspace_id) FastAPI dependency. The dependency only checks that the caller is a member of the workspace_id in the URL prefix. The route handlers then look up the inner resource (agent_id, issue_id, project_id, label_id, comment_id, dependency_id) by primary key alone. The resource's own workspace_id is never compared to the URL's workspace_id. A user can therefore put their own workspace in the URL prefix and any other workspace's resource ID in the path. The auth check passes, since they really are a member of the prefix workspace. The service then returns the cross-tenant resource for read, update, or delete. There is a second bug in the member-management routes (add_member, update_member_role, remove_member, update_workspace, delete_workspace). Each one inherits the default min_role="member" from require_workspace_member. Any basic member can therefore promote themselves to admin or owner, demote or remove other members, and delete the workspace. The role hierarchy exists in the schema but is not enforced. Registration is open at /api/v1/auth/register with no email verification. The default server bind is 0.0.0.0:8000 (python -m praisonai_platform). One curl from any unauthenticated network position is enough to bootstrap into the system. PraisonAI Platform version 0.1.4 patches the issue.
- Record
EUVD-2026-46319- CVSS 4.0
- 9.4 Critical
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H - Updated
- 2026-11-02
GitHub AdvisoriesGHSA-h8q5-cp56-rr65
9.4
PraisonAI Platform has a cross-workspace IDOR + member-role privilege escalation
Summary
The Platform server exposes resources under /api/v1/workspaces/{workspace_id}/... and protects them with a require_workspace_member(workspace_id) FastAPI dependency. The dependency only checks that the caller is a member of the workspace_id in the URL prefix. The route handlers then look up the inner resource (agent_id, issue_id, project_id, label_id, comment_id, dependency_id) by primary key alone. The resource's own workspace_id is never compared to the URL's workspace_id.
A user can therefore put their own workspace in the URL prefix and any other workspace's resource ID in the path. The auth check passes, since they really are a member of the prefix workspace. The service then returns the cross-tenant resource for read, update, or delete.
There is a second bug in the member-management routes (add_member, update_member_role, remove_member, update_workspace, delete_workspace). Each one inherits the default min_role="member" from require_workspace_member. Any basic member can therefore promote themselves to admin or owner, demote or remove other members, and delete the workspace. The role hierarchy exists in the schema but is not enforced.
Registration is open at /api/v1/auth/register with no email verification. The default server bind is 0.0.0.0:8000 (python -m praisonai_platform). One curl from any unauthenticated network position is enough to bootstrap into the system.
Affected functionality
Every nested-resource route under /api/v1/workspaces/{workspace_id}/...:
| File | Routes |
|------|--------|
| routes/agents.py | GET /agents/{agent_id}, PATCH /agents/{agent_id}, DELETE /agents/{agent_id} |
| routes/issues.py | GET /issues/{issue_id}, PATCH /issues/{issue_id}, DELETE /issues/{issue_id}, POST /issues/{issue_id}/comments, GET /issues/{issue_id}/comments |
| routes/projects.py | GET /projects/{project_id}, PATCH /projects/{project_id}, DELETE /projects/{project_id}, GET /projects/{project_id}/stats |
| routes/labels.py | PATCH /labels/{label_id}, DELETE /labels/{label_id}, POST /issues/{issue_id}/labels/{label_id}, DELETE /issues/{issue_id}/labels/{label_id}, GET /issues/{issue_id}/labels |
| routes/dependencies.py | every route |
| routes/workspaces.py | PATCH /{workspace_id}, DELETE /{workspace_id}, POST /{workspace_id}/members, PATCH /{workspace_id}/members/{user_id}, DELETE /{workspace_id}/members/{user_id} (these have a role-enforcement bug rather than a cross-tenant bug) |
Root cause
A. The auth dependency only sees the URL prefix
src/praisonai-platform/praisonai_platform/api/deps.py:54-73:
async def require_workspace_member(
workspace_id: str,
user: AuthIdentity = Depends(get_current_user),
session: AsyncSession = Depends(get_db),
min_role: str = "member",
) -> AuthIdentity:
member_svc = MemberService(session)
has = await member_svc.has_role(workspace_id, user.id, min_role)
if not has:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=...)
user.workspace_id = workspace_id
return user
This only validates that the user is a member of the URL workspace_id. It does not (and cannot, given its signature) validate any inner resource ID.
B. The service-layer lookups are unscoped
Example, src/praisonai-platform/praisonai_platform/services/agent_service.py:53-55:
async def get(self, agent_id: str) -> Optional[Agent]:
return await self._session.get(Agent, agent_id)
And the route, src/praisonai-platform/praisonai_platform/api/routes/agents.py:53-64:
@router.get("/{agent_id}", response_model=AgentResponse)
async def get_agent(workspace_id: str, agent_id: str,
user: AuthIdentity = Depends(require_workspace_member),
session: AsyncSession = Depends(get_db)):
svc = AgentService(session)
agent = await svc.get(agent_id) # ← no…
- Record
GHSA-h8q5-cp56-rr65- CVSS 4.0
- 9.4 Critical
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H - Weakness
- CWE-269, CWE-639, CWE-863
- Updated
- 2026-11-02
NVD
9.4
PraisonAI Platform is the platform layer for the PraisonAI multi-agent teams system. Prior to version 0.1.4, the Platform server exposes resources under /api/v1/workspaces/{workspace_id}/... and protects them with a require_workspace_member(workspace_id) FastAPI dependency. The dependency only checks that the caller is a member of the workspace_id in the URL prefix. The route handlers then look up the inner resource (agent_id, issue_id, project_id, label_id, comment_id, dependency_id) by primary key alone. The resource's own workspace_id is never compared to the URL's workspace_id. A user can therefore put their own workspace in the URL prefix and any other workspace's resource ID in the path. The auth check passes, since they really are a member of the prefix workspace. The service then returns the cross-tenant resource for read, update, or delete. There is a second bug in the member-management routes (add_member, update_member_role, remove_member, update_workspace, delete_workspace). Each one inherits the default min_role="member" from require_workspace_member. Any basic member can therefore promote themselves to admin or owner, demote or remove other members, and delete the workspace. The role hierarchy exists in the schema but is not enforced. Registration is open at /api/v1/auth/register with no email verification. The default server bind is 0.0.0.0:8000 (python -m praisonai_platform). One curl from any unauthenticated network position is enough to bootstrap into the system. PraisonAI Platform version 0.1.4 patches the issue.
- Record
CVE-2026-47407- CVSS 4.0
- 9.4 Critical
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X - Weakness
- CWE-269, CWE-639, CWE-863
- Updated
- 2026-11-02
OSV
9.4
PraisonAI Platform has a cross-workspace IDOR + member-role privilege escalation
PraisonAI Platform is the platform layer for the PraisonAI multi-agent teams system. Prior to version 0.1.4, the Platform server exposes resources under /api/v1/workspaces/{workspace_id}/... and protects them with a require_workspace_member(workspace_id) FastAPI dependency. The dependency only checks that the caller is a member of the workspace_id in the URL prefix. The route handlers then look up the inner resource (agent_id, issue_id, project_id, label_id, comment_id, dependency_id) by primary key alone. The resource's own workspace_id is never compared to the URL's workspace_id. A user can therefore put their own workspace in the URL prefix and any other workspace's resource ID in the path. The auth check passes, since they really are a member of the prefix workspace. The service then returns the cross-tenant resource for read, update, or delete. There is a second bug in the member-management routes (add_member, update_member_role, remove_member, update_workspace, delete_workspace). Each one inherits the default min_role="member" from require_workspace_member. Any basic member can therefore promote themselves to admin or owner, demote or remove other members, and delete the workspace. The role hierarchy exists in the schema but is not enforced. Registration is open at /api/v1/auth/register with no email verification. The default server bind is 0.0.0.0:8000 (python -m praisonai_platform). One curl from any unauthenticated network position is enough to bootstrap into the system. PraisonAI Platform version 0.1.4 patches the issue.
- Record
CVE-2026-47407- CVSS 4.0
- 9.4 Critical
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H - Weakness
- CWE-269, CWE-639, CWE-863
- Updated
- 2026-11-02
OSVGHSA-h8q5-cp56-rr65
9.4
PraisonAI Platform has a cross-workspace IDOR + member-role privilege escalation
Summary
The Platform server exposes resources under /api/v1/workspaces/{workspace_id}/... and protects them with a require_workspace_member(workspace_id) FastAPI dependency. The dependency only checks that the caller is a member of the workspace_id in the URL prefix. The route handlers then look up the inner resource (agent_id, issue_id, project_id, label_id, comment_id, dependency_id) by primary key alone. The resource's own workspace_id is never compared to the URL's workspace_id.
A user can therefore put their own workspace in the URL prefix and any other workspace's resource ID in the path. The auth check passes, since they really are a member of the prefix workspace. The service then returns the cross-tenant resource for read, update, or delete.
There is a second bug in the member-management routes (add_member, update_member_role, remove_member, update_workspace, delete_workspace). Each one inherits the default min_role="member" from require_workspace_member. Any basic member can therefore promote themselves to admin or owner, demote or remove other members, and delete the workspace. The role hierarchy exists in the schema but is not enforced.
Registration is open at /api/v1/auth/register with no email verification. The default server bind is 0.0.0.0:8000 (python -m praisonai_platform). One curl from any unauthenticated network position is enough to bootstrap into the system.
Affected functionality
Every nested-resource route under /api/v1/workspaces/{workspace_id}/...:
| File | Routes |
|------|--------|
| routes/agents.py | GET /agents/{agent_id}, PATCH /agents/{agent_id}, DELETE /agents/{agent_id} |
| routes/issues.py | GET /issues/{issue_id}, PATCH /issues/{issue_id}, DELETE /issues/{issue_id}, POST /issues/{issue_id}/comments, GET /issues/{issue_id}/comments |
| routes/projects.py | GET /projects/{project_id}, PATCH /projects/{project_id}, DELETE /projects/{project_id}, GET /projects/{project_id}/stats |
| routes/labels.py | PATCH /labels/{label_id}, DELETE /labels/{label_id}, POST /issues/{issue_id}/labels/{label_id}, DELETE /issues/{issue_id}/labels/{label_id}, GET /issues/{issue_id}/labels |
| routes/dependencies.py | every route |
| routes/workspaces.py | PATCH /{workspace_id}, DELETE /{workspace_id}, POST /{workspace_id}/members, PATCH /{workspace_id}/members/{user_id}, DELETE /{workspace_id}/members/{user_id} (these have a role-enforcement bug rather than a cross-tenant bug) |
Root cause
A. The auth dependency only sees the URL prefix
src/praisonai-platform/praisonai_platform/api/deps.py:54-73:
async def require_workspace_member(
workspace_id: str,
user: AuthIdentity = Depends(get_current_user),
session: AsyncSession = Depends(get_db),
min_role: str = "member",
) -> AuthIdentity:
member_svc = MemberService(session)
has = await member_svc.has_role(workspace_id, user.id, min_role)
if not has:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=...)
user.workspace_id = workspace_id
return user
This only validates that the user is a member of the URL workspace_id. It does not (and cannot, given its signature) validate any inner resource ID.
B. The service-layer lookups are unscoped
Example, src/praisonai-platform/praisonai_platform/services/agent_service.py:53-55:
async def get(self, agent_id: str) -> Optional[Agent]:
return await self._session.get(Agent, agent_id)
And the route, src/praisonai-platform/praisonai_platform/api/routes/agents.py:53-64:
@router.get("/{agent_id}", response_model=AgentResponse)
async def get_agent(workspace_id: str, agent_id: str,
user: AuthIdentity = Depends(require_workspace_member),
session: AsyncSession = Depends(get_db)):
svc = AgentService(session)
agent = await svc.get(agent_id) # ← no…
- Record
GHSA-h8q5-cp56-rr65- CVSS 4.0
- 9.4 Critical
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H - Weakness
- CWE-269, CWE-639, CWE-863
- Updated
- 2026-11-02
OSVPYSEC-2026-482
9.4
PraisonAI Platform has a cross-workspace IDOR + member-role privilege escalation
Summary
The Platform server exposes resources under /api/v1/workspaces/{workspace_id}/... and protects them with a require_workspace_member(workspace_id) FastAPI dependency. The dependency only checks that the caller is a member of the workspace_id in the URL prefix. The route handlers then look up the inner resource (agent_id, issue_id, project_id, label_id, comment_id, dependency_id) by primary key alone. The resource's own workspace_id is never compared to the URL's workspace_id.
A user can therefore put their own workspace in the URL prefix and any other workspace's resource ID in the path. The auth check passes, since they really are a member of the prefix workspace. The service then returns the cross-tenant resource for read, update, or delete.
There is a second bug in the member-management routes (add_member, update_member_role, remove_member, update_workspace, delete_workspace). Each one inherits the default min_role="member" from require_workspace_member. Any basic member can therefore promote themselves to admin or owner, demote or remove other members, and delete the workspace. The role hierarchy exists in the schema but is not enforced.
Registration is open at /api/v1/auth/register with no email verification. The default server bind is 0.0.0.0:8000 (python -m praisonai_platform). One curl from any unauthenticated network position is enough to bootstrap into the system.
Affected functionality
Every nested-resource route under /api/v1/workspaces/{workspace_id}/...:
| File | Routes |
|------|--------|
| routes/agents.py | GET /agents/{agent_id}, PATCH /agents/{agent_id}, DELETE /agents/{agent_id} |
| routes/issues.py | GET /issues/{issue_id}, PATCH /issues/{issue_id}, DELETE /issues/{issue_id}, POST /issues/{issue_id}/comments, GET /issues/{issue_id}/comments |
| routes/projects.py | GET /projects/{project_id}, PATCH /projects/{project_id}, DELETE /projects/{project_id}, GET /projects/{project_id}/stats |
| routes/labels.py | PATCH /labels/{label_id}, DELETE /labels/{label_id}, POST /issues/{issue_id}/labels/{label_id}, DELETE /issues/{issue_id}/labels/{label_id}, GET /issues/{issue_id}/labels |
| routes/dependencies.py | every route |
| routes/workspaces.py | PATCH /{workspace_id}, DELETE /{workspace_id}, POST /{workspace_id}/members, PATCH /{workspace_id}/members/{user_id}, DELETE /{workspace_id}/members/{user_id} (these have a role-enforcement bug rather than a cross-tenant bug) |
Root cause
A. The auth dependency only sees the URL prefix
src/praisonai-platform/praisonai_platform/api/deps.py:54-73:
async def require_workspace_member(
workspace_id: str,
user: AuthIdentity = Depends(get_current_user),
session: AsyncSession = Depends(get_db),
min_role: str = "member",
) -> AuthIdentity:
member_svc = MemberService(session)
has = await member_svc.has_role(workspace_id, user.id, min_role)
if not has:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=...)
user.workspace_id = workspace_id
return user
This only validates that the user is a member of the URL workspace_id. It does not (and cannot, given its signature) validate any inner resource ID.
B. The service-layer lookups are unscoped
Example, src/praisonai-platform/praisonai_platform/services/agent_service.py:53-55:
async def get(self, agent_id: str) -> Optional[Agent]:
return await self._session.get(Agent, agent_id)
And the route, src/praisonai-platform/praisonai_platform/api/routes/agents.py:53-64:
@router.get("/{agent_id}", response_model=AgentResponse)
async def get_agent(workspace_id: str, agent_id: str,
user: AuthIdentity = Depends(require_workspace_member),
session: AsyncSession = Depends(get_db)):
svc = AgentService(session)
agent = await svc.get(agent_id) # ← no…
- Record
PYSEC-2026-482- CVSS 4.0
- 9.4 Critical
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA: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 |
|---|---|---|
| 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-47407 is a critical vulnerability (CVSS 9.4) 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.30%, the 23rd percentile. Fixed in praisonai-platform 0.1.4.
References
github.com/MervinPraison/PraisonAI/security/advisories/GHSA-h8q5-cp56-rr65
github.com/MervinPraison/PraisonAI/pull/1686
github.com/MervinPraison/PraisonAI/commit/24385d64876577620f749957bd4814f162f4ca47
github.com/advisories/GHSA-h8q5-cp56-rr65
github.com/CVEProject/cvelistV5/tree/main/cves/2026/47xxx/CVE-2026-47407.json
nvd.nist.gov/vuln/detail/CVE-2026-47407
github.com/MervinPraison/PraisonAI
pypi.org/project/praisonai-platform
Left out here: the rest of 3 source descriptions. The JSON record has them.