# PraisonAI Platform workspace-scoped routes allow cross-workspace object access by global object ID

PraisonAI Platform workspace routes authorize URL workspace membership but resolve objects by global UUID, letting any workspace member read, modify, and delete other workspaces' agents.

- Payout: Research
- Severity: high
- Program: PraisonAI Platform
- Found by: beanduan22
- Disclosed: 2026-05-29
- CVE: CVE-2026-47399
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-6h6v-6m7w-7vxx
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-6h6v-6m7w-7vxx
- Tags: area:web domain:api domain:web impact:data-leak impact:integrity kind:research platform:ghsa sev:high tech:python tech:rest technique:source-review vuln:access-control vuln:idor

**Target:** Workspace-scoped REST API routes (agents, projects, issues, comments)
**Root cause:** The route dependency only checks that the caller is a member of the workspace_id in the URL, while service-layer lookups fetch/update/delete objects by global UUID alone without verifying object.workspace_id == workspace_id.
**Steps:**
1. Register attacker and victim accounts; each creates a workspace (workspace_attacker, workspace_victim).
2. Victim creates an agent in workspace_victim; note its global UUID.
3. Attacker sends `GET /api/v1/workspaces/{workspace_attacker}/agents/{victim_agent_id}` with attacker token; server returns victim agent (200, workspace_id=victim).
4. Attacker sends `PATCH /api/v1/workspaces/{workspace_attacker}/agents/{victim_agent_id}` with `{"instructions": "pwned-by-attacker"}`; victim later reads the modified instructions.
5. Attacker sends `DELETE /api/v1/workspaces/{workspace_attacker}/agents/{victim_agent_id}`; returns 204 and victim's subsequent read returns 404.
**How found:** Code review of the route/service split: noticed require_workspace_member validates only URL workspace membership while service methods like `self._session.get(Agent, agent_id)` resolve by global UUID with no workspace_id comparison, then confirmed via an end-to-end HTTP PoC against a real FastAPI app.
**Exploitation:** Any authenticated member of any workspace supplies their own workspace_id in the URL path while passing a victim object's global UUID; the route authorizes on attacker workspace membership, then the service resolves and mutates the victim object by UUID, enabling read, update, and delete across tenant boundaries.
**Escalation:** Same pattern extends to projects, issues, and comments (comment listing pivots from raw issue_id), broadening cross-tenant read/modify/delete of project and issue state.
**Insight:** When a route is workspace-scoped but the service layer takes only an object ID, the membership check and ownership check are decoupled — grep for service methods whose signatures lack workspace_id and for `session.get(Model, id)` lookups, then test by swapping in a foreign UUID under your own workspace path.

## Proof of concept (copied exactly from the original report)

**Attacker reads victim agent via attacker workspace path**

```
attacker_read = await client.get(
            f"/api/v1/workspaces/{attacker_workspace_id}/agents/{victim_agent_id}",
            headers=attacker_headers,
        )
```

**Attacker updates and deletes victim agent**

```
attacker_update = await client.patch(
            f"/api/v1/workspaces/{attacker_workspace_id}/agents/{victim_agent_id}",
            json={"instructions": "pwned-by-attacker"},
            headers=attacker_headers,
        )
```

