{"id":"CVE-2026-46424","aliases":["EUVD-2026-32597","GHSA-6vp2-6r7m-2jvx"],"status":"published","title":"Budibase: Missing Cache Invalidation on Public API Role Unassignment Allows Revoked Users to Retain Privileges for Up to 1 Hour","description":"Budibase is an open-source low-code platform. Prior to 3.38.2, the 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. This vulnerability is fixed in 3.38.2.","severity":{"version":"3.1","score":4.2,"vector":"CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:L/I:L/A:N","level":"medium","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-10","score":0.00163,"percentile":0.05549,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":false,"fix_available":true},"cwes":["CWE-269"],"published":"2026-05-19T16:30:38Z","modified":"2026-10-28T02:31:08Z","fixes":[{"ecosystem":"npm","package":"@budibase/backend-core","fixed_in":["3.38.2"],"sources":["ghsa","osv"]}],"affected":[{"source":"ghsa","ecosystem":"npm","package":"@budibase/backend-core","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"3.38.2"}]}]},{"source":"osv","ecosystem":"npm","package":"@budibase/backend-core","ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"3.38.2"}]}]}],"products":[{"vendor":"budibase","product":"budibase"}],"references":[{"url":"https://github.com/Budibase/budibase/security/advisories/GHSA-6vp2-6r7m-2jvx","tags":["x_refsource_CONFIRM","exploit","advisory","web"]},{"url":"https://github.com/Budibase/budibase/releases/tag/3.38.2","tags":["x_refsource_MISC","web"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-46424","tags":["advisory"]},{"url":"https://github.com/advisories/GHSA-6vp2-6r7m-2jvx"},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/46xxx/CVE-2026-46424.json","tags":["advisory"]},{"url":"https://github.com/Budibase/budibase","tags":["package"]}],"sources":{"cve":[{"id":"CVE-2026-46424","title":"Budibase: Missing Cache Invalidation on Public API Role Unassignment Allows Revoked Users to Retain Privileges for Up to 1 Hour","description":"Budibase is an open-source low-code platform. Prior to 3.38.2, the 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. This vulnerability is fixed in 3.38.2.","status":"published","published":"2026-05-27T17:05:21.894Z","modified":"2026-05-28T14:08:42.106Z","cvss":[{"version":"3.1","score":4.2,"vector":"CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:L/I:L/A:N","level":"medium","assessor":"cna"}],"cwes":["CWE-269"],"updated_at":"2026-11-02T13:46:55.086623Z"}],"euvd":[{"id":"EUVD-2026-32597","description":"Budibase is an open-source low-code platform. Prior to 3.38.2, the 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. This vulnerability is fixed in 3.38.2.","status":"published","published":"2026-05-27T17:05:21Z","modified":"2026-05-28T14:08:42Z","cvss":[{"version":"3.1","score":4.2,"vector":"CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:L/I:L/A:N","level":"medium"}],"updated_at":"2026-11-02T18:21:02.581093Z"}],"ghsa":[{"id":"GHSA-6vp2-6r7m-2jvx","title":"Budibase: Missing Cache Invalidation on Public API Role Unassignment Allows Revoked Users to Retain Privileges for Up to 1 Hour","description":"## 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.","status":"published","published":"2026-05-19T16:30:38Z","modified":"2026-06-08T23:51:53Z","cvss":[{"version":"3.1","score":4.2,"vector":"CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:L/I:L/A:N","level":"medium"}],"cwes":["CWE-269"],"updated_at":"2026-11-02T14:40:21.48274Z"}],"nvd":[{"id":"CVE-2026-46424","description":"Budibase is an open-source low-code platform. Prior to 3.38.2, the 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. This vulnerability is fixed in 3.38.2.","status":"published","published":"2026-05-27T18:16:26.15Z","modified":"2026-06-17T10:53:39.61Z","cvss":[{"version":"3.1","score":4.2,"vector":"CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:L/I:L/A:N","level":"medium","assessor":"secondary"}],"cwes":["CWE-269"],"updated_at":"2026-11-02T14:12:20.885865Z"}],"osv":[{"id":"CVE-2026-46424","title":"Budibase: Missing Cache Invalidation on Public API Role Unassignment Allows Revoked Users to Retain Privileges for Up to 1 Hour","description":"Budibase is an open-source low-code platform. Prior to 3.38.2, the 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. This vulnerability is fixed in 3.38.2.","status":"published","published":"2026-05-27T17:05:21.894Z","modified":"2026-10-28T02:31:08.620420283Z","cvss":[{"version":"3.1","score":4.2,"vector":"CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:L/I:L/A:N","level":"medium"}],"cwes":["CWE-269"],"updated_at":"2026-11-02T15:45:05.870292Z"},{"id":"GHSA-6vp2-6r7m-2jvx","title":"Budibase: Missing Cache Invalidation on Public API Role Unassignment Allows Revoked Users to Retain Privileges for Up to 1 Hour","description":"## 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.","status":"published","published":"2026-05-19T16:30:38Z","modified":"2026-09-10T03:50:45.892361723Z","cvss":[{"version":"3.1","score":4.2,"vector":"CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:L/I:L/A:N","level":"medium"}],"cwes":["CWE-269"],"updated_at":"2026-11-02T15:46:40.308826Z"}]}}
