{"id":"GHSA-6vp2-6r7m-2jvx","summary":"Budibase: Missing Cache Invalidation on Public API Role Unassignment Allows Revoked Users to Retain Privileges for Up to 1 Hour","details":"## Summary\n\nThe public API role unassignment endpoint (`POST /api/public/v1/roles/unassign`) updates user documents in CouchDB but does not invalidate the corresponding Redis user cache entries. Because the authentication middleware resolves user identity and permissions from this cache (TTL: 3600 seconds), a user whose admin, builder, or app-level roles have been revoked via the public API retains those privileges for up to 1 hour.\n\n## Details\n\nThe root cause is an inconsistency between the `UserDB.save()` and `UserDB.bulkUpdate()` code paths.\n\n**Vulnerable path** — `packages/pro/src/sdk/publicApi/roles.ts:49-75`:\n```typescript\nexport async function unAssign(userIds: string[], opts: AssignmentOpts) {\n  // ... modifies user objects: deletes roles, admin, builder ...\n  await userDB.bulkUpdate(users)  // line 74\n}\n```\n\n`bulkUpdate` delegates to `bulkUpdateGlobalUsers()` at `packages/backend-core/src/users/users.ts:82-85`:\n```typescript\nexport async function bulkUpdateGlobalUsers(users: User[]) {\n  const db = getGlobalDB()\n  return (await db.bulkDocs(users)) as BulkDocsResponse\n}\n```\n\nThis writes directly to CouchDB with **no cache invalidation**.\n\n**Correct path** — `packages/backend-core/src/users/db.ts:355` (used by admin UI):\n```typescript\nawait cache.user.invalidateUser(response.id)\n```\n\n**Cache configuration** — `packages/backend-core/src/cache/user.ts:11`:\n```typescript\nconst EXPIRY_SECONDS = 3600  // 1 hour TTL\n```\n\n**Authentication middleware** — `packages/backend-core/src/middleware/authenticated.ts:153-160`:\n```typescript\nuser = await getUser({\n  userId,\n  tenantId: session.tenantId,\n  email: session.email,\n})\n```\n\n`getUser()` reads from Redis cache first; it only falls back to CouchDB on cache miss. After `unAssign` updates CouchDB without invalidating Redis, every authenticated request continues to use the stale cached user object with the old (revoked) privileges.\n\nNotably, other bulk operations in the codebase handle this correctly — `groups.addUsers()` and `groups.removeUsers()` in `packages/pro/src/sdk/groups/groups.ts` both loop through affected users and call `cache.user.invalidateUser()` after `bulkUpdateGlobalUsers()`. The public API roles path was missed.\n\n## PoC\n\n```bash\n# Prerequisites: Enterprise license, admin API key, a second user with admin role\n\n# Step 1: Confirm user has admin access\ncurl -s -X GET http://localhost:10000/api/global/roles \\\n  -H 'Cookie: budibase:auth=\u003ctarget-user-session\u003e' \\\n  -H 'x-budibase-app-id: app_xyz'\n# Returns 200 with roles list\n\n# Step 2: Revoke admin role via public API\ncurl -s -X POST http://localhost:10000/api/public/v1/roles/unassign \\\n  -H 'x-budibase-api-key: \u003cadmin-api-key\u003e' \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"userIds\": [\"\u003ctarget-user-id\u003e\"], \"admin\": true}'\n# Returns 200 — role removed from CouchDB\n\n# Step 3: Verify DB was updated (admin field removed)\n# (check CouchDB directly - user document no longer has admin: {global: true})\n\n# Step 4: Immediately retry admin endpoint as revoked user\ncurl -s -X GET http://localhost:10000/api/global/roles \\\n  -H 'Cookie: budibase:auth=\u003ctarget-user-session\u003e' \\\n  -H 'x-budibase-app-id: app_xyz'\n# STILL returns 200 — stale cache serves old admin privileges\n\n# Step 5: Wait for cache expiry (up to 3600 seconds) and retry\n# After cache expires, the request correctly returns 403\n```\n\n## Impact\n\nA user whose admin, builder, or app-level roles have been revoked via the public API retains full access to those privileges for up to 1 hour. This is particularly concerning in automated offboarding scenarios where HR/IT systems use the public API to revoke access for terminated employees — the terminated user retains admin/builder access to all applications and data during the cache window.\n\nThe impact is bounded by:\n- Requires enterprise license (expanded public API feature)\n- Maximum 1-hour window before cache expires\n- Only affects the public API revocation path; revocations via the admin UI (`UserDB.save()`) invalidate cache correctly\n- The `assign` direction has the inverse issue (newly granted roles are delayed) but this is less security-critical\n\n## Recommended Fix\n\nAdd cache invalidation to `bulkUpdateGlobalUsers` or to the callers that need it. The most targeted fix is in the `unAssign` function:\n\n```typescript\n// packages/pro/src/sdk/publicApi/roles.ts\nimport { cache } from \"@budibase/backend-core\"\n\nexport async function unAssign(userIds: string[], opts: AssignmentOpts) {\n  // ... existing role removal logic ...\n  await userDB.bulkUpdate(users)\n  \n  // Invalidate cache for all affected users\n  await Promise.all(\n    users.map(user =\u003e cache.user.invalidateUser(user._id!))\n  )\n}\n```\n\nAlternatively, fix it at the `bulkUpdate` level to prevent future callers from having the same gap:\n\n```typescript\n// packages/backend-core/src/users/db.ts\nstatic async bulkUpdate(users: User[]) {\n  const result = await usersCore.bulkUpdateGlobalUsers(users)\n  await Promise.all(\n    users.map(user =\u003e cache.user.invalidateUser(user._id!))\n  )\n  return result\n}\n```\n\nThe same fix should also be applied to the `assign` function in the same file.","aliases":["CVE-2026-46424"],"modified":"2026-09-10T03:50:45.892361723Z","published":"2026-05-19T16:30:38Z","database_specific":{"nvd_published_at":"2026-05-27T18:16:26Z","cwe_ids":["CWE-269"],"severity":"MODERATE","github_reviewed":true,"github_reviewed_at":"2026-05-19T16:30:38Z"},"references":[{"type":"WEB","url":"https://github.com/Budibase/budibase/security/advisories/GHSA-6vp2-6r7m-2jvx"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-46424"},{"type":"PACKAGE","url":"https://github.com/Budibase/budibase"},{"type":"WEB","url":"https://github.com/Budibase/budibase/releases/tag/3.38.2"}],"affected":[{"package":{"name":"@budibase/backend-core","ecosystem":"npm","purl":"pkg:npm/%40budibase/backend-core"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"3.38.2"}]}],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/05/GHSA-6vp2-6r7m-2jvx/GHSA-6vp2-6r7m-2jvx.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:L/I:L/A:N"}]}