GitHub Advisoryadvisory · npm

GHSA-hp6v-6jw7-gv2f Budibase: OIDC SSO account takeover: incoming identity linked by email without checking email_verified

CRITICAL Published 2026-07-24 Modified 2026-08-12 Official page ↗
Aliases
CVE-2026-73302
Ecosystem
npm
Packages
@budibase/server
Severity
Critical
CVSS vector
CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H
Weaknesses
CWE-287
Reviewed
2026-07-24

Affected packages

• npm @budibase/server: <= 3.38.1 (no fix yet)

Description

Summary Budibase's OIDC SSO login links an incoming SSO identity to an existing Budibase account by email address alone, without ever checking the `email_verified` claim of the OIDC ID token. Budibase first tries to match the IdP `sub`; when that misses (any fresh attacker IdP account) it silently falls back to matching by the `email` claim and merges into the existing account by email, preserving that account's `_id` and roles. Because the `email_verified` flag is never read, an attacker who can make a configured/trusted IdP emit a token carrying `email = ` with `email_verified = false` is logged into Budibase as the victim, inheriting the victim's roles (including global admin/builder). Per OIDC Core §5.7 the `email` claim MUST NOT be used as an identity key unless `email_verified` is `true`; Budibase effectively delegates all account-linking trust to every configured IdP's email-verification policy while checking nothing itself. Full account takeover of any existing Budibase user, including the instance owner. Details The OIDC verify callback extracts the email and never consults `email_verified`: • `packages/backend-core/src/middleware/passport/sso/oidc.ts:59` — `email: getEmail(profile, jwtClaims)`. • `getEmail` (`oidc.ts:113-135`) returns `profile._json.email` ->`jwtClaims.email` -> `preferred_username`. No `email_verified` check. • `buildJwtClaims` (`oidc.ts:99-107`) assembles claims from `_json.email`/`emails[0].value` — no verification flag is read. `grep -r email_verified packages/` -> 0 hits. The email is then used as the account-linking key: • `sso.authenticate(...)` -> `packages/backend-core/src/middleware/passport/sso/sso.ts`: • `:38,44` `users.getById(generateGlobalUserID(details.userId))` keyed on the IdP `sub`; for a fresh attacker IdP account this 404s and is swallowed (`:45-54`). • `:57-59` fallback: `dbUser = await users.getGlobalUserByEmail(details.email)` -> loads the victim's account (victim `_id` + roles) purely by email (`packages/backend-core/src/users/users.ts:100-124`, `USER_BY_EMAIL` view, no binding to the IdP `sub`). • `syncUser(...)` (`sso.ts:80,102-138`) spreads `...user`, preserving the victim `_id`/`tenantId`/`roles`; only overwrites provider fields. • `UserDB.save` (`packages/backend-core/src/users/db.ts:235`): because `ssoUser._id` is the victim's, the `_id` branch runs (`:253`), `getById(_id)` matches the victim (`:256`), the "Email address cannot be changed" guard (`:257-259`) does not fire (`dbUser.email === email`), and the `EmailUnavailableError` guard (`:269-275`) is skipped (it only runs in the `!dbUser` branch). The merge proceeds silently; a session JWT is issued for the victim. Per OIDC Core §5.7, the `email` claim MUST NOT be used as an identity key unless `email_verified` is `true`. Budibase never reads the flag. Preconditions (attack requirement — `AT:P`): the attacker must be able to authenticate through an IdP that the Budibase instance trusts AND get that IdP to assert the victim's email with `email_verified = false`. This is reachable, not exotic: • **Self-registration with an unverified email — Keycloak and Authentik ship with *"Verify Email" OFF by default*; if the trusted IdP allows public sign-up, the attacker registers a new account and simply enters `email = ` at sign-up. No confirmation email is needed — the IdP stores and asserts it unverified. • Self-service profile editing — many IdPs let a logged-in user change their own email without forced re-verification. • Attacker-operated / federated IdP or permissive social login — where the attacker controls or influences a trusted provider, or the provider asserts a user-typed (unverified) email. It is not exploitable through a strict corporate IdP that enforces email verification (there `email_verified = true` and the attacker cannot claim the victim's address) — which is exactly why Budibase must check the flag rather than assume every configured IdP enforces it. The defect is unconditional on the Budibase side; the attack requirement is purely the (default, common) IdP email policy. PoC Reproduced live on Budibase `3.39.14` (self-hosted, community license) against a stock Keycloak 26 realm `budi` with default "Verify Email" = off; OIDC client registered and activated in Budibase. Setup: a pre-existing victim** global-admin Budibase account `[email protected]` (`_id = us_1ab2dfcf…`, local account, *no IdP link*). The attacker owns their own IdP account (`attacker`, distinct `sub`) and can set its email attribute unverified. Step 1 — the IdP asserts the claim (proves `email_verified=false`): ``` POST /realms/budi/protocol/openid-connect/token (Keycloak) grant_type=password&client_id=budibase&client_secret=…&username=attacker&password=Attacker123!&scope=openid email profile -> id_token payload: { "sub":"3cf58c45-…", "preferred_username":"attacker", "email":"[email protected]", "email_verified":false } ``` The authenticated principal is provably `attacker` (its own `sub`/`preferred_username`/password), merely *claiming* the victim's email, unverified. Step 2 — drive the standard OIDC flow as `attacker`: `GET /api/global/auth/default/oidc/configs/kc-oidc-1` -> IdP login as `attacker`/`Attacker123!` -> `GET /api/global/auth/oidc/callback?code=…&state=…`. Step 3 — result (takeover): Budibase sets `budibase:auth` to a session JWT `{ "userId":"us_1ab2dfcf…", "email":"[email protected]", "tenantId":"default" }`, and `GET /api/global/self` returns the victim: `_id = us_1ab2dfcf…`, `admin.global = true`, `builder.global = true`, `providerType = oidc`. The attacker authenticated as a *different* IdP principal with an *unverified* email yet now holds a full global-admin session for the victim. Negative control (proves the email claim is the cause, not a normal self-login): A second attacker `attacker2` with a benign unverified email `[email protected]` (matching no Budibase user) runs the *identical* flow: ``` id_token: { "sub":"55794bf2-…", "preferred_username":"attacker2", "email":"[email protected]", "email_verified":false } -> budibase:auth: { "userId":"us_55794bf2-…" } (a NEW account, _id derived from the IdP sub) -> /api/global/self: { "_id":"us_55794bf2-…", "email":"[email protected]", admin.global: null, builder.global: null } ``` With a benign email the attacker gets their own new low-privilege account; only when the email claim equals the victim's does the same flow yield the victim's admin account. Same self-authentication, single variable changed = the unverified-email merge is the vulnerability. Impact Takeover of any existing Budibase account by email, including the instance owner / global …

Related

Alias · CVE (1)

  • CVE-2026-73302 Budibase is an open-source low-code platform. vulnerability

Weakness · CWE (1)

  • CWE-287 Improper Authentication weakness

References