medium

GHSA-fcrw-f7gg-6g9f

npm · @budibase/server

Summary

Budibase: SSO OAuth2 Token Leakage via User Metadata Endpoints to Power-Role Users

Severity
medium
CVSS
4.9
CWE
CWE-200
Published
2026-07-24
Updated
2026-07-24

Advisory details

Summary

The /api/users/metadata and /api/users/metadata/:id endpoints in @budibase/server return full global user profiles to any user with POWER role or above. For SSO-authenticated users (OIDC, Google), the response includes oauth2.accessToken and oauth2.refreshToken fields, leaking identity provider credentials to other users who should not have access to them.

Details

When a user authenticates via SSO (OIDC or Google), the OAuth2 tokens are stored in the global CouchDB user document at packages/backend-core/src/auth/auth.ts:170-173:

dbUser.oauth2 = {
  ...dbUser.oauth2,
  ...details,
}
await db.put(dbUser)

The user metadata endpoints are protected by PermissionType.USER, PermissionLevel.READ (packages/server/src/api/routes/user.ts:11), which maps to the POWER permission set (packages/backend-core/src/security/permissions.ts:99).

Path 1 — List all users (GET /api/users/metadata):

  1. controller.fetchMetadata → sdk.users.fetchMetadata() (packages/server/src/sdk/users/utils.ts:78)
  2. → getGlobalUsers() → getRawGlobalUsers() (packages/server/src/utilities/global.ts:101-122) — strips only password and forceResetPassword
  3. → processUser() (packages/server/src/utilities/global.ts:15-76) — strips only password and roles
  4. The oauth2 field containing accessToken and refreshToken is never removed

Path 2 — Single user (GET /api/users/metadata/:id):

  1. controller.findMetadata → getFullUser() (packages/server/src/utilities/users.ts:6)
  2. → getGlobalUser() → getRawGlobalUser() (packages/server/src/utilities/global.ts:90-92) — raw CouchDB fetch, no field stripping at all
  3. → processUser() — strips only password and roles
  4. Same result: oauth2 tokens are returned

There is no output sanitization middleware on these routes that would strip sensitive fields before the response reaches the client.

PoC

Prerequisites: A Budibase instance with at least one SSO-authenticated user (OIDC or Google) and a separate user with POWER role in an app.

Step 1 — As the POWER user, list all users:

curl -s -X GET 'http://localhost:10000/api/users/metadata' \
  -H 'Cookie: budibase:auth=<power-user-jwt>' \
  -H 'x-budibase-app-id: app_<appid>' | jq '.[].oauth2'

Expected output: null for all users (tokens should not be exposed)

Actual output: For SSO users, the response includes:

{
  "accessToken": "ya29.a0ARrdaM...",
  "refreshToken": "1//0eXxXxXxXx..."
}

Step 2 — Fetch a specific SSO user's profile:

curl -s -X GET 'http://localhost:10000/api/users/metadata/ro_ta_users_us_<sso-user-id>' \
  -H 'Cookie: budibase:auth=<power-user-jwt>' \
  -H 'x-budibase-app-id: app_<appid>' | jq '.oauth2'

This also returns the full OAuth2 tokens.

Impact

Recommended Fix

Strip sensitive SSO fields in processUser() at packages/server/src/utilities/global.ts:15:

export async function processUser(
  user: ContextUser,
  opts: { appId?: string; groups?: UserGroup[] } = {}
) {
  if (!user || (!user.roles && !user.userGroups)) {
    return user
  }
  user = cloneDeep(user)
  delete user.password
+ delete (user as any).oauth2
+ delete (user as any).provider
+ delete (user as any).providerType
+ delete (user as any).thirdPartyProfile
+ delete (user as any).profile
+ delete (user as any).ssoId
+ delete (user as any).forceResetPassword
  // ... rest of function

Additionally, getRawGlobalUsers() at line 101 should also strip oauth2 alongside its existing password/forceResetPassword stripping for defense in depth.

References

Related advisories

Is your project exposed to this? Stateward checks every dependency on every pull request and flags it only if your code actually reaches it.

Check my repo

Summarize with AI

ChatGPTClaudePerplexity

Sources: CISA KEV (public domain), OSV.dev & GitHub Advisory Database (CC-BY-4.0), FIRST EPSS, NVD/CWE (public domain). Served live from the Stateward advisory database.