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.
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,
...
) ->...
0.1.00.1.10.1.20.1.30.1.4Exploitability
AV:NAC:LPR:LUI:NScope
S:UImpact
C:HI:HA:LCVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:L