# Budibase: Privilege escalation via public role assignment API missing app-level authorization

App-scoped Budibase builder self-issues an API key and calls /api/public/v1/roles/assign with appBuilder/role props to gain builder access to any tenant app.

- Payout: Research
- Severity: high
- Program: Budibase
- Found by: dinhvaren
- Disclosed: 2026-07-24
- CVE: CVE-2026-73305
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-j9fc-w3mr-x6mv
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-j9fc-w3mr-x6mv
- Tags: area:web domain:api domain:web impact:data-leak impact:internal-access impact:privesc kind:research platform:ghsa sev:high technique:incomplete-fix technique:source-review vuln:access-control vuln:auth-bypass vuln:privilege-escalation

**Target:** POST /api/public/v1/roles/assign public API endpoint
**Root cause:** Authorization is a flag-level allowlist validating only global admin/builder; the appBuilder and role:{appId,roleId} grant vectors pass to the SDK unvalidated, so no check exists that the caller controls the target app.
**Steps:**
1. Attacker holds app-scoped builder on appA only (user.builder.apps=[appA]).
2. `POST /api/global/self/api_key` on worker passes builderOnly via hasBuilderPermissions → obtain public API key.
3. `POST /api/public/v1/roles/assign` with header `x-budibase-app-id: <appA prod id>` and body `{"userIds":["<self>"],"appBuilder":{"appId":"<appB>"}}`.
4. builderOrAdmin passes via isBuilder(user, appA); validateGlobalRoleUpdate ignores appBuilder; SDK appends appB to user.builder.apps.
5. Repeat with `{"userIds":["<self>"],"role":{"appId":"<appB>","roleId":"ADMIN"}}` to set arbitrary data-plane role.
**How found:** Source review of the incomplete fix in commit d63d1d9054: noticed validateGlobalRoleUpdate only references admin/builder flags while the GlobalRoleUpdate interface omits appBuilder/role, then traced route middleware (builderOrAdmin only, no authorized(USER)) and SDK assign() writing unvalidated props.
**Exploitation:** Self-issue API key via builderOnly-gated endpoint, then call roles/assign scoped to own app while targeting victim app in body; SDK writes appB into user.builder.apps and user.roles[appB]=ADMIN, yielding builder of appB: read/modify rows, read datasource configs and exfiltrate stored credentials, edit automations with bash/executeScript steps.
**Escalation:** Cross-app builder grants access to every other app in tenant; role vector allows arbitrary data-plane role assignment including ADMIN to any user.
**Insight:** When a fix hardens an endpoint by allowlisting specific body fields, enumerate every other field the downstream SDK consumes — here appBuilder/role were silently passed through, and the middleware's workspaceId check could be satisfied by pointing x-budibase-app-id at an app the attacker already controls.

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

**HTTP PoC: self-issue key then escalate via roles/assign**

```
# 1) self-issue API key
curl -X POST http://localhost:10000/api/global/self/api_key -H "Cookie: <attacker session>" -d '{}'
# 2) escalate: grant self builder of appB
curl -X POST http://localhost:10000/api/public/v1/roles/assign \
  -H "x-budibase-api-key: <KEY>" -H "x-budibase-app-id: <appA prod id>" \
  -H "content-type: application/json" \
  -d '{"userIds":["<self global id>"],"appBuilder":{"appId":"<appB>"}}'
```

**Unit harness command proving unvalidated appBuilder/role**

```
cd D:/CVE-Hunting/budibase-audit-output/lab
node harness/verify-privesc-authz.cjs
```

