CVE-2026-47419
praisonai-platform: Agent endpoints accept any agent_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 agent CRUD endpoints (GET / PATCH / DELETE /workspaces/{workspace_id}/agents/{agent_id}) gate access on require_workspace_member(workspace_id) only, then resolve agent_id through AgentService.get(agent_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 agents that belong to a different workspace W2 by guessing or harvesting an agent UUID and calling …/workspaces/W1/agents/<W2-agent-id>. 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: Agent endpoints accept any agent_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 agent CRUD endpoints (GET / PATCH / DELETE /workspaces/{workspace_id}/agents/{agent_id}) gate access on require_workspace_member(workspace_id) only, then resolve agent_id through AgentService.get(agent_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 agents that belong to a different workspace W2 by guessing or harvesting an agent UUID and calling …/workspaces/W1/agents/<W2-agent-id>. PraisonAI Platform version 0.1.4 patches the issue.
- Record
CVE-2026-47419- 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-46339
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 agent CRUD endpoints (GET / PATCH / DELETE /workspaces/{workspace_id}/agents/{agent_id}) gate access on require_workspace_member(workspace_id) only, then resolve agent_id through AgentService.get(agent_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 agents that belong to a different workspace W2 by guessing or harvesting an agent UUID and calling …/workspaces/W1/agents/<W2-agent-id>. PraisonAI Platform version 0.1.4 patches the issue.
- Record
EUVD-2026-46339- 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-7p8g-6c6g-h9w7
8.3
praisonai-platform: Agent endpoints accept any agent_id without workspace ownership check, cross-workspace read/update/delete IDOR
Summary
Type: Insecure Direct Object Reference. The agent CRUD endpoints (GET / PATCH / DELETE /workspaces/{workspace_id}/agents/{agent_id}) gate access on require_workspace_member(workspace_id) only, then resolve agent_id through AgentService.get(agent_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 agents that belong to a different workspace W2 by guessing or harvesting an agent UUID and calling …/workspaces/W1/agents/<W2-agent-id>.
File: src/praisonai-platform/praisonai_platform/services/agent_service.py, lines 53-112; route handlers at src/praisonai-platform/praisonai_platform/api/routes/agents.py, lines 53-100.
Root cause: the route extracts workspace_id from the URL path and passes it to require_workspace_member for the membership check, but never threads it through to the service layer. AgentService.get calls session.get(Agent, agent_id), which is SELECT * FROM agents WHERE id = :agent_id with no AND workspace_id = :workspace_id. update and delete call self.get(agent_id) first and then mutate the returned row, inheriting the same gap. The MemberService is the one place in this codebase that does this correctly: it uses (workspace_id, user_id) as a composite key. The agent service simply forgot the second predicate, which is the textbook GHSA pattern for FastAPI services that treat routing parameters as decorative rather than authoritative.
Affected Code
File 1: src/praisonai-platform/praisonai_platform/services/agent_service.py, lines 53-55 and 105-112.
class AgentService:
...
async def get(self, agent_id: str) -> Optional[Agent]:
"""Get agent by ID."""
return await self._session.get(Agent, agent_id) # <-- BUG: no workspace_id predicate
async def update(
self,
agent_id: str,
name: Optional[str] = None,
...
) -> Optional[Agent]:
agent = await self.get(agent_id) # <-- inherits the same gap
if agent is None:
return None
...
return agent
async def delete(self, agent_id: str) -> bool:
agent = await self.get(agent_id) # <-- inherits the same gap
if agent is None:
return False
await self._session.delete(agent)
await self._session.flush()
return True
File 2: src/praisonai-platform/praisonai_platform/api/routes/agents.py, lines 53-101.
@router.get("/{agent_id}", response_model=AgentResponse)
async def get_agent(
workspace_id: str,
agent_id: str,
user: AuthIdentity = Depends(require_workspace_member), # only checks membership in workspace_id
session: AsyncSession = Depends(get_db),
):
svc = AgentService(session)
agent = await svc.get(agent_id) # <-- workspace_id never passed; svc.get returns any agent in the DB
if agent is None:
raise HTTPException(status_code=404, detail="Agent not found")
return AgentResponse.model_validate(agent)
The update_agent (lines 67-87) and delete_agent (lines 90-100) handlers exhibit the same pattern: they receive workspace_id via path parameter, use it solely for the membership gate, then call svc.update(agent_id, ...) / svc.delete(agent_id) without re-checking which workspace the agent actually belongs to.
Why it's wrong: the workspace_id segment in the route is treated as a UI hint (it gates "are you in some workspace W?") rather than an authoritative predicate (it should also gate "is the resource you are addressing actually inside W?"). A standard fix in FastAPI/SQLAlchemy services is to make the resource-lookup query include the workspace predicate and treat absence as 404, so that a foreign-workspace agent is indistinguishable from a non-existent one. The codebase already does this…
- Record
GHSA-7p8g-6c6g-h9w7- 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 agent CRUD endpoints (GET / PATCH / DELETE /workspaces/{workspace_id}/agents/{agent_id}) gate access on require_workspace_member(workspace_id) only, then resolve agent_id through AgentService.get(agent_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 agents that belong to a different workspace W2 by guessing or harvesting an agent UUID and calling …/workspaces/W1/agents/<W2-agent-id>. PraisonAI Platform version 0.1.4 patches the issue.
- Record
CVE-2026-47419- 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: Agent endpoints accept any agent_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 agent CRUD endpoints (GET / PATCH / DELETE /workspaces/{workspace_id}/agents/{agent_id}) gate access on require_workspace_member(workspace_id) only, then resolve agent_id through AgentService.get(agent_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 agents that belong to a different workspace W2 by guessing or harvesting an agent UUID and calling …/workspaces/W1/agents/<W2-agent-id>. PraisonAI Platform version 0.1.4 patches the issue.
- Record
CVE-2026-47419- 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-7p8g-6c6g-h9w7
8.3
praisonai-platform: Agent endpoints accept any agent_id without workspace ownership check, cross-workspace read/update/delete IDOR
Summary
Type: Insecure Direct Object Reference. The agent CRUD endpoints (GET / PATCH / DELETE /workspaces/{workspace_id}/agents/{agent_id}) gate access on require_workspace_member(workspace_id) only, then resolve agent_id through AgentService.get(agent_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 agents that belong to a different workspace W2 by guessing or harvesting an agent UUID and calling …/workspaces/W1/agents/<W2-agent-id>.
File: src/praisonai-platform/praisonai_platform/services/agent_service.py, lines 53-112; route handlers at src/praisonai-platform/praisonai_platform/api/routes/agents.py, lines 53-100.
Root cause: the route extracts workspace_id from the URL path and passes it to require_workspace_member for the membership check, but never threads it through to the service layer. AgentService.get calls session.get(Agent, agent_id), which is SELECT * FROM agents WHERE id = :agent_id with no AND workspace_id = :workspace_id. update and delete call self.get(agent_id) first and then mutate the returned row, inheriting the same gap. The MemberService is the one place in this codebase that does this correctly: it uses (workspace_id, user_id) as a composite key. The agent service simply forgot the second predicate, which is the textbook GHSA pattern for FastAPI services that treat routing parameters as decorative rather than authoritative.
Affected Code
File 1: src/praisonai-platform/praisonai_platform/services/agent_service.py, lines 53-55 and 105-112.
class AgentService:
...
async def get(self, agent_id: str) -> Optional[Agent]:
"""Get agent by ID."""
return await self._session.get(Agent, agent_id) # <-- BUG: no workspace_id predicate
async def update(
self,
agent_id: str,
name: Optional[str] = None,
...
) -> Optional[Agent]:
agent = await self.get(agent_id) # <-- inherits the same gap
if agent is None:
return None
...
return agent
async def delete(self, agent_id: str) -> bool:
agent = await self.get(agent_id) # <-- inherits the same gap
if agent is None:
return False
await self._session.delete(agent)
await self._session.flush()
return True
File 2: src/praisonai-platform/praisonai_platform/api/routes/agents.py, lines 53-101.
@router.get("/{agent_id}", response_model=AgentResponse)
async def get_agent(
workspace_id: str,
agent_id: str,
user: AuthIdentity = Depends(require_workspace_member), # only checks membership in workspace_id
session: AsyncSession = Depends(get_db),
):
svc = AgentService(session)
agent = await svc.get(agent_id) # <-- workspace_id never passed; svc.get returns any agent in the DB
if agent is None:
raise HTTPException(status_code=404, detail="Agent not found")
return AgentResponse.model_validate(agent)
The update_agent (lines 67-87) and delete_agent (lines 90-100) handlers exhibit the same pattern: they receive workspace_id via path parameter, use it solely for the membership gate, then call svc.update(agent_id, ...) / svc.delete(agent_id) without re-checking which workspace the agent actually belongs to.
Why it's wrong: the workspace_id segment in the route is treated as a UI hint (it gates "are you in some workspace W?") rather than an authoritative predicate (it should also gate "is the resource you are addressing actually inside W?"). A standard fix in FastAPI/SQLAlchemy services is to make the resource-lookup query include the workspace predicate and treat absence as 404, so that a foreign-workspace agent is indistinguishable from a non-existent one. The codebase already does this…
- Record
GHSA-7p8g-6c6g-h9w7- 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-2931
8.3
praisonai-platform: Agent endpoints accept any agent_id without workspace ownership check, cross-workspace read/update/delete IDOR
Summary
Type: Insecure Direct Object Reference. The agent CRUD endpoints (GET / PATCH / DELETE /workspaces/{workspace_id}/agents/{agent_id}) gate access on require_workspace_member(workspace_id) only, then resolve agent_id through AgentService.get(agent_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 agents that belong to a different workspace W2 by guessing or harvesting an agent UUID and calling …/workspaces/W1/agents/<W2-agent-id>.
File: src/praisonai-platform/praisonai_platform/services/agent_service.py, lines 53-112; route handlers at src/praisonai-platform/praisonai_platform/api/routes/agents.py, lines 53-100.
Root cause: the route extracts workspace_id from the URL path and passes it to require_workspace_member for the membership check, but never threads it through to the service layer. AgentService.get calls session.get(Agent, agent_id), which is SELECT * FROM agents WHERE id = :agent_id with no AND workspace_id = :workspace_id. update and delete call self.get(agent_id) first and then mutate the returned row, inheriting the same gap. The MemberService is the one place in this codebase that does this correctly: it uses (workspace_id, user_id) as a composite key. The agent service simply forgot the second predicate, which is the textbook GHSA pattern for FastAPI services that treat routing parameters as decorative rather than authoritative.
Affected Code
File 1: src/praisonai-platform/praisonai_platform/services/agent_service.py, lines 53-55 and 105-112.
class AgentService:
...
async def get(self, agent_id: str) -> Optional[Agent]:
"""Get agent by ID."""
return await self._session.get(Agent, agent_id) # <-- BUG: no workspace_id predicate
async def update(
self,
agent_id: str,
name: Optional[str] = None,
...
) -> Optional[Agent]:
agent = await self.get(agent_id) # <-- inherits the same gap
if agent is None:
return None
...
return agent
async def delete(self, agent_id: str) -> bool:
agent = await self.get(agent_id) # <-- inherits the same gap
if agent is None:
return False
await self._session.delete(agent)
await self._session.flush()
return True
File 2: src/praisonai-platform/praisonai_platform/api/routes/agents.py, lines 53-101.
@router.get("/{agent_id}", response_model=AgentResponse)
async def get_agent(
workspace_id: str,
agent_id: str,
user: AuthIdentity = Depends(require_workspace_member), # only checks membership in workspace_id
session: AsyncSession = Depends(get_db),
):
svc = AgentService(session)
agent = await svc.get(agent_id) # <-- workspace_id never passed; svc.get returns any agent in the DB
if agent is None:
raise HTTPException(status_code=404, detail="Agent not found")
return AgentResponse.model_validate(agent)
The update_agent (lines 67-87) and delete_agent (lines 90-100) handlers exhibit the same pattern: they receive workspace_id via path parameter, use it solely for the membership gate, then call svc.update(agent_id, ...) / svc.delete(agent_id) without re-checking which workspace the agent actually belongs to.
Why it's wrong: the workspace_id segment in the route is treated as a UI hint (it gates "are you in some workspace W?") rather than an authoritative predicate (it should also gate "is the resource you are addressing actually inside W?"). A standard fix in FastAPI/SQLAlchemy services is to make the resource-lookup query include the workspace predicate and treat absence as 404, so that a foreign-workspace agent is indistinguishable from a non-existent one. The codebase already does this…
- Record
PYSEC-2026-2931- 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-47419 is a high vulnerability (CVSS 8.3) in Mervinpraison Praisonai-platform, published June 5, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.34%, the 28th percentile. Fixed in praisonai-platform 0.1.4.
References
github.com/MervinPraison/PraisonAI/security/advisories/GHSA-7p8g-6c6g-h9w7
github.com/MervinPraison/PraisonAI/commit/ef79b7a0561796ad9807f0f09538c25cc78d3619
github.com/advisories/GHSA-7p8g-6c6g-h9w7
github.com/CVEProject/cvelistV5/tree/main/cves/2026/47xxx/CVE-2026-47419.json
nvd.nist.gov/vuln/detail/CVE-2026-47419
github.com/MervinPraison/PraisonAI
pypi.org/project/praisonai-platform
Left out here: the rest of 3 source descriptions. The JSON record has them.