GHSA-HR66-5MQR-8MPX

Vulnerability from github – Published: 2026-07-24 21:25 – Updated: 2026-07-24 21:25
VLAI
Summary
Budibase: Unauthenticated user information disclosure via public tenant user lookup endpoint
Details

Summary

The Budibase Worker service exposes a public, unauthenticated API endpoint (GET /api/global/users/tenant/:id) that returns sensitive user information including tenantId, userId, email, and ssoId. The endpoint is registered in the PUBLIC_ENDPOINTS list with a TODO comment acknowledging it "should be an internal API." Any unauthenticated party can enumerate user emails or IDs to extract sensitive tenant and user metadata, enabling targeted attacks against multi-tenant deployments.

Details

Public endpoint registration at packages/worker/src/api/index.ts lines 56-59:

// TODO: This should be an internal api
{
  route: "/api/global/users/tenant/:id",
  method: "GET",
},

This endpoint is listed in PUBLIC_ENDPOINTS, which is passed to auth.buildAuthMiddleware(PUBLIC_ENDPOINTS) at line 154. When a request matches a public endpoint pattern, the authentication middleware sets ctx.publicEndpoint = true and calls next() without performing any authentication (verified at packages/backend-core/src/middleware/authenticated.ts lines 124-126, 249-251).

All subsequent middleware also skips for public endpoints: - buildTenancyMiddleware — passes through - activeTenant — passes through - buildCsrfMiddleware — skipped for GET methods (line 48 of csrf.ts) - The budibaseAccess gate at lines 160-168 explicitly returns next() when ctx.publicEndpoint is true

Route registration at packages/worker/src/api/routes/global/users.ts line 139:

loggedInRoutes
  .get("/api/global/users/tenant/:id", controller.tenantUserLookup)

loggedInRoutes has no auth middleware group — it is created with endpointGroupList.group() (no middleware).

Handler implementation at packages/worker/src/api/controllers/global/users.ts lines 548-562:

export const tenantUserLookup = async (
  ctx: UserCtx<void, LookupTenantUserResponse>
) => {
  const id = ctx.params.id
  // is email, check its valid
  if (id.includes("@") && !emailValidator.validate(id)) {
    ctx.throw(400, `${id} is not a valid email address to lookup.`)
  }
  const user = await userSdk.core.getFirstPlatformUser(id)
  if (user) {
    ctx.body = user    // Returns full PlatformUser object — no field filtering
  } else {
    ctx.throw(400, "No tenant user found.")
  }
}

The id parameter accepts either an email address (detected by @ presence) or a user ID. The response returns the full PlatformUser object from packages/types/src/documents/platform/users.ts:

export interface PlatformUserByEmail extends Document {
  tenantId: string    // Tenant identifier
  userId: string      // Internal user ID
}

export interface PlatformUserById extends Document {
  tenantId: string    // Tenant identifier
  email?: string      // User email address
  ssoId?: string      // SSO provider identifier
}

export interface PlatformUserBySsoId extends Document {
  tenantId: string    // Tenant identifier
  userId: string      // Internal user ID
  email: string       // User email address
  ssoId?: string      // SSO provider identifier
}

The lookup function (packages/backend-core/src/users/lookup.ts:48-53) queries the PLATFORM_USERS_LOWERCASE CouchDB view with include_docs: true, returning the complete platform user document including CouchDB _id and _rev.

Affected files: - packages/worker/src/api/index.ts:56-59 — Public endpoint registration - packages/worker/src/api/routes/global/users.ts:139 — Route on unauthenticated group - packages/worker/src/api/controllers/global/users.ts:548-562 — Handler returning full user object - packages/backend-core/src/users/lookup.ts:48-53 — Platform user lookup with include_docs: true - packages/types/src/documents/platform/users.ts:6-36 — PlatformUser types

PoC

Static verification:

  1. Observe packages/worker/src/api/index.ts:56-59: endpoint in PUBLIC_ENDPOINTS with // TODO: This should be an internal api
  2. Trace handler at packages/worker/src/api/controllers/global/users.ts:548-562: no auth checks, returns ctx.body = user (full object)
  3. Trace middleware chain: all middleware passes through for ctx.publicEndpoint === true
  4. Confirm no field filtering, sanitization, or authorization between request and response

Dynamic verification (requires running Budibase instance with at least one user):

# No authentication headers or cookies required
# Lookup by email:
curl -s http://localhost:4002/api/global/users/tenant/admin@example.com

# Response (200 OK):
# {
#   "_id": "admin@example.com",
#   "_rev": "1-abc123...",
#   "tenantId": "tenant-uuid-here",
#   "userId": "us_uuid-here"
# }

# Lookup by user ID:
curl -s http://localhost:4002/api/global/users/tenant/us_someuserid123

# Response (200 OK):
# {
#   "_id": "us_someuserid123",
#   "_rev": "1-abc123...",
#   "tenantId": "tenant-uuid-here",
#   "email": "admin@example.com",
#   "ssoId": "google-oauth-id"
# }

# Non-existent user:
curl -s http://localhost:4002/api/global/users/tenant/nonexistent@example.com
# Response: 400 "No tenant user found."
# (Different response confirms user enumeration)

Negative case: Requesting a non-existent user returns HTTP 400 with "No tenant user found.", while an existing user returns HTTP 200 with full data. The different status codes confirm user existence, enabling enumeration.

Impact

This is a CWE-200: Exposure of Sensitive Information to an Unauthorized Actor vulnerability.

Who is impacted: All Budibase deployments — both self-hosted and cloud. The impact is highest for multi-tenant (cloud) deployments where tenant IDs are security boundaries and user enumeration across tenants enables targeted attacks.

An unauthenticated attacker can: 1. Enumerate all user accounts by testing known or guessed email addresses against the endpoint 2. Extract tenant IDs for any known user, enabling targeted cross-tenant attacks 3. Extract user IDs (userId) for use in other API calls or attacks 4. Extract SSO identifiers (ssoId) which may link to external identity providers (Google, OIDC) 5. Confirm user existence through different HTTP responses (200 vs 400) 6. Harvest CouchDB revision tokens (_rev) which could assist in CouchDB-level attacks

The returned tenant IDs are particularly dangerous in multi-tenant deployments because they identify the security boundary between organizations. Combined with the hardcoded session keys (separate finding), an attacker could use enumerated tenant IDs to craft targeted session fixation attacks.

Suggested remediation

  1. Remove the endpoint from PUBLIC_ENDPOINTS and move it to internal-only routes, as the TODO comment at line 56 already suggests
  2. Add authentication and authorization if the endpoint must remain accessible — require at least builderOrAdmin role
  3. Limit returned fields to only what the consumer actually needs (strip _rev, ssoId, and other sensitive fields)
  4. Return generic 404 for both "not found" and "access denied" to prevent user enumeration
  5. Add rate limiting to prevent automated mass enumeration
  6. Regression test: Add a test verifying GET /api/global/users/tenant/:id returns 403 without authentication
Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "npm",
        "name": "@budibase/server"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "last_affected": "3.38.1"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [],
  "database_specific": {
    "cwe_ids": [
      "CWE-200"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-07-24T21:25:00Z",
    "nvd_published_at": null,
    "severity": "HIGH"
  },
  "details": "#### Summary\nThe Budibase Worker service exposes a public, unauthenticated API endpoint (`GET /api/global/users/tenant/:id`) that returns sensitive user information including `tenantId`, `userId`, `email`, and `ssoId`. The endpoint is registered in the `PUBLIC_ENDPOINTS` list with a `TODO` comment acknowledging it \"should be an internal API.\" Any unauthenticated party can enumerate user emails or IDs to extract sensitive tenant and user metadata, enabling targeted attacks against multi-tenant deployments.\n\n#### Details\n\n**Public endpoint registration** at `packages/worker/src/api/index.ts` lines 56-59:\n\n```typescript\n// TODO: This should be an internal api\n{\n  route: \"/api/global/users/tenant/:id\",\n  method: \"GET\",\n},\n```\n\nThis endpoint is listed in `PUBLIC_ENDPOINTS`, which is passed to `auth.buildAuthMiddleware(PUBLIC_ENDPOINTS)` at line 154. When a request matches a public endpoint pattern, the authentication middleware sets `ctx.publicEndpoint = true` and calls `next()` without performing any authentication (verified at `packages/backend-core/src/middleware/authenticated.ts` lines 124-126, 249-251).\n\nAll subsequent middleware also skips for public endpoints:\n- `buildTenancyMiddleware` \u2014 passes through\n- `activeTenant` \u2014 passes through\n- `buildCsrfMiddleware` \u2014 skipped for GET methods (line 48 of csrf.ts)\n- The `budibaseAccess` gate at lines 160-168 explicitly returns `next()` when `ctx.publicEndpoint` is true\n\n**Route registration** at `packages/worker/src/api/routes/global/users.ts` line 139:\n\n```typescript\nloggedInRoutes\n  .get(\"/api/global/users/tenant/:id\", controller.tenantUserLookup)\n```\n\n`loggedInRoutes` has no auth middleware group \u2014 it is created with `endpointGroupList.group()` (no middleware).\n\n**Handler implementation** at `packages/worker/src/api/controllers/global/users.ts` lines 548-562:\n\n```typescript\nexport const tenantUserLookup = async (\n  ctx: UserCtx\u003cvoid, LookupTenantUserResponse\u003e\n) =\u003e {\n  const id = ctx.params.id\n  // is email, check its valid\n  if (id.includes(\"@\") \u0026\u0026 !emailValidator.validate(id)) {\n    ctx.throw(400, `${id} is not a valid email address to lookup.`)\n  }\n  const user = await userSdk.core.getFirstPlatformUser(id)\n  if (user) {\n    ctx.body = user    // Returns full PlatformUser object \u2014 no field filtering\n  } else {\n    ctx.throw(400, \"No tenant user found.\")\n  }\n}\n```\n\nThe `id` parameter accepts either an email address (detected by `@` presence) or a user ID. The response returns the **full** `PlatformUser` object from `packages/types/src/documents/platform/users.ts`:\n\n```typescript\nexport interface PlatformUserByEmail extends Document {\n  tenantId: string    // Tenant identifier\n  userId: string      // Internal user ID\n}\n\nexport interface PlatformUserById extends Document {\n  tenantId: string    // Tenant identifier\n  email?: string      // User email address\n  ssoId?: string      // SSO provider identifier\n}\n\nexport interface PlatformUserBySsoId extends Document {\n  tenantId: string    // Tenant identifier\n  userId: string      // Internal user ID\n  email: string       // User email address\n  ssoId?: string      // SSO provider identifier\n}\n```\n\nThe lookup function (`packages/backend-core/src/users/lookup.ts:48-53`) queries the `PLATFORM_USERS_LOWERCASE` CouchDB view with `include_docs: true`, returning the complete platform user document including CouchDB `_id` and `_rev`.\n\n**Affected files:**\n- `packages/worker/src/api/index.ts:56-59` \u2014 Public endpoint registration\n- `packages/worker/src/api/routes/global/users.ts:139` \u2014 Route on unauthenticated group\n- `packages/worker/src/api/controllers/global/users.ts:548-562` \u2014 Handler returning full user object\n- `packages/backend-core/src/users/lookup.ts:48-53` \u2014 Platform user lookup with `include_docs: true`\n- `packages/types/src/documents/platform/users.ts:6-36` \u2014 PlatformUser types\n\n#### PoC\n\n**Static verification:**\n\n1. Observe `packages/worker/src/api/index.ts:56-59`: endpoint in `PUBLIC_ENDPOINTS` with `// TODO: This should be an internal api`\n2. Trace handler at `packages/worker/src/api/controllers/global/users.ts:548-562`: no auth checks, returns `ctx.body = user` (full object)\n3. Trace middleware chain: all middleware passes through for `ctx.publicEndpoint === true`\n4. Confirm no field filtering, sanitization, or authorization between request and response\n\n**Dynamic verification (requires running Budibase instance with at least one user):**\n\n```bash\n# No authentication headers or cookies required\n# Lookup by email:\ncurl -s http://localhost:4002/api/global/users/tenant/admin@example.com\n\n# Response (200 OK):\n# {\n#   \"_id\": \"admin@example.com\",\n#   \"_rev\": \"1-abc123...\",\n#   \"tenantId\": \"tenant-uuid-here\",\n#   \"userId\": \"us_uuid-here\"\n# }\n\n# Lookup by user ID:\ncurl -s http://localhost:4002/api/global/users/tenant/us_someuserid123\n\n# Response (200 OK):\n# {\n#   \"_id\": \"us_someuserid123\",\n#   \"_rev\": \"1-abc123...\",\n#   \"tenantId\": \"tenant-uuid-here\",\n#   \"email\": \"admin@example.com\",\n#   \"ssoId\": \"google-oauth-id\"\n# }\n\n# Non-existent user:\ncurl -s http://localhost:4002/api/global/users/tenant/nonexistent@example.com\n# Response: 400 \"No tenant user found.\"\n# (Different response confirms user enumeration)\n```\n\n**Negative case:** Requesting a non-existent user returns HTTP 400 with `\"No tenant user found.\"`, while an existing user returns HTTP 200 with full data. The different status codes confirm user existence, enabling enumeration.\n\n#### Impact\nThis is a **CWE-200: Exposure of Sensitive Information to an Unauthorized Actor** vulnerability.\n\n**Who is impacted:** All Budibase deployments \u2014 both self-hosted and cloud. The impact is highest for multi-tenant (cloud) deployments where tenant IDs are security boundaries and user enumeration across tenants enables targeted attacks.\n\nAn unauthenticated attacker can:\n1. **Enumerate all user accounts** by testing known or guessed email addresses against the endpoint\n2. **Extract tenant IDs** for any known user, enabling targeted cross-tenant attacks\n3. **Extract user IDs** (`userId`) for use in other API calls or attacks\n4. **Extract SSO identifiers** (`ssoId`) which may link to external identity providers (Google, OIDC)\n5. **Confirm user existence** through different HTTP responses (200 vs 400)\n6. **Harvest CouchDB revision tokens** (`_rev`) which could assist in CouchDB-level attacks\n\nThe returned tenant IDs are particularly dangerous in multi-tenant deployments because they identify the security boundary between organizations. Combined with the hardcoded session keys (separate finding), an attacker could use enumerated tenant IDs to craft targeted session fixation attacks.\n\n### Suggested remediation\n1. **Remove the endpoint from `PUBLIC_ENDPOINTS`** and move it to internal-only routes, as the TODO comment at line 56 already suggests\n2. **Add authentication and authorization** if the endpoint must remain accessible \u2014 require at least `builderOrAdmin` role\n3. **Limit returned fields** to only what the consumer actually needs (strip `_rev`, `ssoId`, and other sensitive fields)\n4. **Return generic 404** for both \"not found\" and \"access denied\" to prevent user enumeration\n5. **Add rate limiting** to prevent automated mass enumeration\n6. **Regression test:** Add a test verifying `GET /api/global/users/tenant/:id` returns 403 without authentication",
  "id": "GHSA-hr66-5mqr-8mpx",
  "modified": "2026-07-24T21:25:00Z",
  "published": "2026-07-24T21:25:00Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/Budibase/budibase/security/advisories/GHSA-hr66-5mqr-8mpx"
    },
    {
      "type": "WEB",
      "url": "https://github.com/Budibase/budibase/pull/19221"
    },
    {
      "type": "WEB",
      "url": "https://github.com/Budibase/budibase/commit/e6bf245fbfdaa35804ef7ee901103282edf0c381"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/Budibase/budibase"
    },
    {
      "type": "WEB",
      "url": "https://github.com/Budibase/budibase/releases/tag/3.39.32"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": " Budibase: Unauthenticated user information disclosure via public tenant user lookup endpoint"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…

Loading…