high · 8.8Jul 24, 2026

Budibase: App-Scoped Builder Privilege Escalation via Public Role Assignment API

Pranav Khune
Penetration Testing Team Lead, SecureLayer7

A user who is only a builder for one specific app can grant themselves builder access to every other app in the tenant, or assign any user an admin role in any app, by calling the public…

Package@budibase/server
Ecosystemnpm
Affected<= 3.38.1
Budibase: App-Scoped Builder Privilege Escalation via Public Role Assignment API

The problem

The POST /api/public/v1/roles/assign endpoint in @budibase/server is protected by builderOrAdmin middleware. That middleware only checks whether the caller is a builder for the app ID supplied in the x-budibase-app-id header. An app-scoped builder passes that check by supplying their own app.

The validateGlobalRoleUpdate function only validates the admin and builder flags. It ignores the appBuilder and role fields entirely. Those fields flow unvalidated into the SDK, which writes them directly to the target user's document, pushing any arbitrary app ID into user.builder.apps or setting any role in user.roles.

The attacker can also self-issue a public API key via POST /api/global/self/api_key, because that endpoint's builderOnly gate passes anyone with hasBuilderPermissions, which is true for app-scoped builders.

Proof of concept

A working proof-of-concept for this issue in @budibase/server, with the exact payload below.

bash
# Step 1: self-issue a public API key (builderOnly passes for app-scoped builders)
curl -X POST http://localhost:10000/api/global/self/api_key \
  -H "Cookie: <attacker_session>" \
  -d '{}'

# Step 2: escalate — grant self builder access to appB
# x-budibase-app-id is set to appA (the app the attacker legitimately builds)
# appBuilder.appId targets appB (an app the attacker has no access to)
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":["<attacker_global_user_id>"],"appBuilder":{"appId":"<appB_id>"}}'

The root cause is a flag-level allowlist instead of a scope check. validateGlobalRoleUpdate only looks at roleUpdate.admin and roleUpdate.builder. The GlobalRoleUpdate TypeScript interface never declares appBuilder or role, so those properties pass through the spread operator into the SDK with zero validation.

The SDK's assign() function then blindly concatenates the supplied appBuilder.appId onto user.builder.apps, and writes opts.role.roleId directly into user.roles[appId], with no check that the caller has authority over the target app. CWE-269 (Improper Privilege Management) and CWE-862 (Missing Authorization) both apply.

The fix in 3.40.0 extends validateGlobalRoleUpdate to also gate the appBuilder and role fields, verifying the caller is actually a builder of the target app before allowing the grant to proceed.

The fix

Upgrade @budibase/server (and the full Budibase suite) to version 3.40.0 or later. The fix extends validateGlobalRoleUpdate in globalRoleValidation.ts to validate appBuilder and role assignment vectors, checking that the caller holds builder authority over the target app before the SDK write is permitted.

No workaround is available short of disabling the expanded public API feature (EXPANDED_PUBLIC_API) on Business/Enterprise plans.

Reporter not attributed.

References: [1][2][3]

Related research