Budibase: App-Scoped Builder Privilege Escalation via Public Role Assignment API
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…

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.
# 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.
Related research
- high · 7.7Budibase S3 Presigned URL Authorization Regression (BASIC User Escalation)
- highBudibase Email Change IDOR Allows Full Account Takeover via POST /api/v2/email
- high · 7.1Budibase MongoDB Integration NoSQL Operator Injection
- high · 8.5Budibase REST Datasource SSRF via DNS Rebinding (undici dispatcher bypasses IP pin)