Server: Budibase: Privilege escalation via public role assignment API missing app-level authorization (CVE-2026-73305)
### 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: - `packages/server/src/api/controllers/public/globalRoleValidation.ts` — `validateGlobalRoleUpdate(ctx, roleUpdate)` only checks `roleUpdate.admin` (requires `isAdmin`) and `roleUpdate.builder` (requires `isGlobalBuilder`). The `GlobalRoleUpdate` interface declares only `{ builder?, admin? }`; `appBuilder` and `role` are not referenced. - `packages/server/src/api/controllers/public/roles.ts` — `assign()` does `const { userIds, ...assignmentProps } = ctx.request.body; validateGlobalRoleUpdate(ctx, assignmentProps); await sdk.publicApi.roles.assign(userIds, assignmentProps)`. The `appBuilder`/`role` props pass through unvalidated. - `packages/pro/src/sdk/publicApi/roles.ts` — `assign()`: for `opts.appBuilder` it sets `user.builder = { apps: existing.concat([getProdWorkspaceID(opts.appBuilder.appId)]) }`; for `opts.role` it sets `user.roles[getProdWorkspaceID(opts.role.appId)] = opts.role.roleId`. No check that the caller builds the target app, and `userIds` is an arbitrary list (`bulkGet`). The only gate is the `isExpandedPublicApiEnabled()` license check. - `packages/server/src/api/routes/public/index.ts` — `applyAdminRoutes(roleEndpoints)` attaches **only** `middleware.builderOrAdmin` (no `publicApi`, no `authorized(PermissionType.USER, …)`). - `packages/backend-core/src/middleware/builderOrAdmin.ts` — with a `workspaceId` present, it only requires `isBuilder(ctx.user, workspaceId)`. The attacker sets `x-budibase-app-id` to **their own** app (appA), so the gate passes. - `packages/backend-core/src/middleware/builderOnly.ts` — on the worker, `POST /api/global/self/api_key` only requires `hasBuilderPermissions(ctx.user)`, which is true for app-scoped builders, so the attacker can self-issue a public API key. - `packages/shared-core/src/sdk/documents/users.ts` — `isGlobalBuilder` is false for app-scoped builders (so the global `builder` flag is correctly blocked), while `isBuilder(user, appA)` and `hasBuilderPermissions(user)` are true. 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: - **Before:** builder of `appA` only. - **After:** builder of `appB` (and any other app) → read/modify all rows, read datasource configs and exfiltrate stored datasource credentials, edit automations (including the `bash`/`executeScript`/`executeQuery` steps); plus arbitrary data-role assignment in any app. - **Why disallowed:** the per-app builder model is meant to isolate builders to their assigned apps; a builder of one app must not gain authority over apps they were never granted. ### PoC Environment: - Budibase version: `3.39.19`, commit `03fbabae4` - Deployment: Business/Enterprise license required (`isExpandedPublicApiEnabled`) - Attacker role: app-scoped builder (`user.builder.apps=[appA]`), not global builder/admin - Mock services: none needed for the unit-level proof; HTTP PoC script provided for a licensed lab Steps (unit-level proof, no license needed — verbatim auth-gate + SDK logic): 1. Run the verification harness: ```bash cd D:/CVE-Hunting/budibase-audit-output/lab node harness/verify-privesc-authz.cjs ``` 2. Observed result (evidence: `evidence/lab-privesc-authz.log`): ```text [PASS-VALIDATION] appBuilder:{appId:APP_B}
AI Analysis
Technical Summary
Budibase versions up to 3.38.1 suffer from a missing authorization check in the public role-assignment API endpoint POST /api/public/v1/roles/assign. The endpoint validates only global role flags ('admin' and 'builder') but does not verify whether the caller has permission to assign app-scoped roles such as 'appBuilder' or arbitrary data-plane roles within specific apps. An attacker with app-scoped builder privileges on one app can exploit this by calling the endpoint with their own app ID in the header and specifying other apps in the role assignment payload, thereby granting themselves elevated roles on any app in the tenant. This flaw arises from an incomplete fix that only hardened global role validation, leaving app-level role assignments unchecked. The vulnerability allows crossing the security boundary from limited app builder access to full builder or admin access on any app, enabling unauthorized data access, configuration changes, and automation edits.
Potential Impact
An attacker with app-scoped builder privileges on a single app can escalate their privileges to become a builder or admin on any other app within the same tenant. This breaks the intended isolation between apps, allowing unauthorized read and modification of data, access to datasource configurations including credentials, and the ability to alter automations and execute arbitrary scripts. The compromise affects confidentiality, integrity, and availability of all apps in the tenant. The vulnerability requires only low attack complexity and no user interaction, making it a high-severity risk.
Mitigation Recommendations
Patch status is not yet confirmed — check the vendor advisory for current remediation guidance. Until an official fix is available, restrict app-scoped builder roles carefully and monitor for suspicious role-assignment API usage. Avoid exposing the public role-assignment API to untrusted users. Review and harden authorization logic to ensure app-level role assignments require verification that the caller controls the target app. Follow vendor updates for a security patch addressing this missing authorization check.
Server: Budibase: Privilege escalation via public role assignment API missing app-level authorization (CVE-2026-73305)
Description
### 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: - `packages/server/src/api/controllers/public/globalRoleValidation.ts` — `validateGlobalRoleUpdate(ctx, roleUpdate)` only checks `roleUpdate.admin` (requires `isAdmin`) and `roleUpdate.builder` (requires `isGlobalBuilder`). The `GlobalRoleUpdate` interface declares only `{ builder?, admin? }`; `appBuilder` and `role` are not referenced. - `packages/server/src/api/controllers/public/roles.ts` — `assign()` does `const { userIds, ...assignmentProps } = ctx.request.body; validateGlobalRoleUpdate(ctx, assignmentProps); await sdk.publicApi.roles.assign(userIds, assignmentProps)`. The `appBuilder`/`role` props pass through unvalidated. - `packages/pro/src/sdk/publicApi/roles.ts` — `assign()`: for `opts.appBuilder` it sets `user.builder = { apps: existing.concat([getProdWorkspaceID(opts.appBuilder.appId)]) }`; for `opts.role` it sets `user.roles[getProdWorkspaceID(opts.role.appId)] = opts.role.roleId`. No check that the caller builds the target app, and `userIds` is an arbitrary list (`bulkGet`). The only gate is the `isExpandedPublicApiEnabled()` license check. - `packages/server/src/api/routes/public/index.ts` — `applyAdminRoutes(roleEndpoints)` attaches **only** `middleware.builderOrAdmin` (no `publicApi`, no `authorized(PermissionType.USER, …)`). - `packages/backend-core/src/middleware/builderOrAdmin.ts` — with a `workspaceId` present, it only requires `isBuilder(ctx.user, workspaceId)`. The attacker sets `x-budibase-app-id` to **their own** app (appA), so the gate passes. - `packages/backend-core/src/middleware/builderOnly.ts` — on the worker, `POST /api/global/self/api_key` only requires `hasBuilderPermissions(ctx.user)`, which is true for app-scoped builders, so the attacker can self-issue a public API key. - `packages/shared-core/src/sdk/documents/users.ts` — `isGlobalBuilder` is false for app-scoped builders (so the global `builder` flag is correctly blocked), while `isBuilder(user, appA)` and `hasBuilderPermissions(user)` are true. 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: - **Before:** builder of `appA` only. - **After:** builder of `appB` (and any other app) → read/modify all rows, read datasource configs and exfiltrate stored datasource credentials, edit automations (including the `bash`/`executeScript`/`executeQuery` steps); plus arbitrary data-role assignment in any app. - **Why disallowed:** the per-app builder model is meant to isolate builders to their assigned apps; a builder of one app must not gain authority over apps they were never granted. ### PoC Environment: - Budibase version: `3.39.19`, commit `03fbabae4` - Deployment: Business/Enterprise license required (`isExpandedPublicApiEnabled`) - Attacker role: app-scoped builder (`user.builder.apps=[appA]`), not global builder/admin - Mock services: none needed for the unit-level proof; HTTP PoC script provided for a licensed lab Steps (unit-level proof, no license needed — verbatim auth-gate + SDK logic): 1. Run the verification harness: ```bash cd D:/CVE-Hunting/budibase-audit-output/lab node harness/verify-privesc-authz.cjs ``` 2. Observed result (evidence: `evidence/lab-privesc-authz.log`): ```text [PASS-VALIDATION] appBuilder:{appId:APP_B}
CVSS v3.1
Score 8.8high
Affected software
Run on your own infrastructure? Check whether these packages are installed with threat-finder — our free open-source scanner.
AI-Powered Analysis
Machine-generated threat intelligence
Technical Analysis
Budibase versions up to 3.38.1 suffer from a missing authorization check in the public role-assignment API endpoint POST /api/public/v1/roles/assign. The endpoint validates only global role flags ('admin' and 'builder') but does not verify whether the caller has permission to assign app-scoped roles such as 'appBuilder' or arbitrary data-plane roles within specific apps. An attacker with app-scoped builder privileges on one app can exploit this by calling the endpoint with their own app ID in the header and specifying other apps in the role assignment payload, thereby granting themselves elevated roles on any app in the tenant. This flaw arises from an incomplete fix that only hardened global role validation, leaving app-level role assignments unchecked. The vulnerability allows crossing the security boundary from limited app builder access to full builder or admin access on any app, enabling unauthorized data access, configuration changes, and automation edits.
Potential Impact
An attacker with app-scoped builder privileges on a single app can escalate their privileges to become a builder or admin on any other app within the same tenant. This breaks the intended isolation between apps, allowing unauthorized read and modification of data, access to datasource configurations including credentials, and the ability to alter automations and execute arbitrary scripts. The compromise affects confidentiality, integrity, and availability of all apps in the tenant. The vulnerability requires only low attack complexity and no user interaction, making it a high-severity risk.
Mitigation Recommendations
Patch status is not yet confirmed — check the vendor advisory for current remediation guidance. Until an official fix is available, restrict app-scoped builder roles carefully and monitor for suspicious role-assignment API usage. Avoid exposing the public role-assignment API to untrusted users. Review and harden authorization logic to ensure app-level role assignments require verification that the caller controls the target app. Follow vendor updates for a security patch addressing this missing authorization check.
Technical Details
- Gcve Source
- db.gcve.eu
- Osv Id
- GHSA-j9fc-w3mr-x6mv
- Osv Schema Version
- 1.4.0
- Aliases
- []
- Ecosystems
- ["npm"]
- Database Specific Severity
- HIGH
- Cvss Version
- 3.1
Threat ID: 6a6542309c2644c7f808a772
Added to database: 07/25/2026, 23:09:36 UTC
Last enriched: 07/25/2026, 23:58:00 UTC
Last updated: 09/08/2026, 05:22:41 UTC
Views: 92
Community Reviews
0 reviewsCrowdsource mitigation strategies, share intel context, and vote on the most helpful responses. Sign in to add your voice and help keep defenders ahead.
Want to contribute mitigation steps or threat intel context? Sign in or create an account to join the community discussion.
Actions
Updates to AI analysis require Pro Console access. Upgrade inside Console → Billing.
External Links
Need more coverage?
Upgrade to Pro Console for AI refresh and higher limits.
For incident response and remediation, OffSeq services can help resolve threats faster.
Latest Threats
Check if your credentials are on the dark web
Instant breach scanning across billions of leaked records. Free tier available.