Summary
Budibase: OIDC SSO account takeover: incoming identity linked by email without checking email_verified
Advisory details
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 = <victim> 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) returnsprofile._json.email->jwtClaims.email->preferred_username. Noemail_verifiedcheck.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,44users.getById(generateGlobalUserID(details.userId))keyed on the IdPsub; for a fresh attacker IdP account this 404s and is swallowed (:45-54).:57-59fallback: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_EMAILview, no binding to the IdPsub).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): becausessoUser._idis the victim's, the_idbranch runs (:253),getById(_id)matches the victim (:256), the "Email address cannot be changed" guard (:257-259) does not fire (dbUser.email === email), and theEmailUnavailableErrorguard (:269-275) is skipped (it only runs in the!dbUserbranch). 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 = <victim>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 = trueand 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 victim@stand.local (_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":"victim@stand.local", "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":"victim@stand.local", "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 attacker2@evil.local (matching no Budibase user) runs the identical flow:
id_token: { "sub":"55794bf2-…", "preferred_username":"attacker2", "email":"attacker2@evil.local", "email_verified":false }
-> budibase:auth: { "userId":"us_55794bf2-…" } (a NEW account, _id derived from the IdP sub)
-> /api/global/self: { "_id":"us_55794bf2-…", "email":"attacker2@evil.local", 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
References
Related vulnerabilities
All Supply chain →- MEDIUMCVE-2026-73840
OpenChoreo: Unauthenticated build/workflow trigger via git-provider confusion (webhook signature bypass)
- HIGHCVE-2026-62669
Grav: 2FA Bypass via 'login.regenerate2FASecret' - Secret Rotation During Pending Challenge
- HIGHCVE-2026-77567
Filament: Multi-factor authentication (app) can be bypassed when recovery codes are enabled
- MEDIUMCVE-2026-55678
arc has unauthenticated cluster node admission when `cluster.shared_secret` is unset
- HIGHCVE-2026-55761
Portainer has Unauthenticated Restore Endpoint that Allows Admin Takeover on Uninitialized Instances
- HIGHCVE-2026-55533
PraisonAI: Authentication fail-open in Recipe server allows unauthenticated access when API key or JWT auth is configured without a secret