{"id":"CVE-2026-73302","aliases":["EUVD-2026-58525","GHSA-hp6v-6jw7-gv2f"],"status":"published","title":"Budibase: OIDC SSO account takeover: incoming identity linked by email without checking email_verified","description":"Budibase is an open-source low-code platform. Prior to 3.39.30, the OIDC flow in packages/backend-core/src/middleware/passport/sso/oidc.ts resolved an email without getEmailVerified or an email_verified requirement, and packages/backend-core/src/middleware/passport/sso/sso.ts then used users.getGlobalUserByEmail as a fallback account-linking key. An attacker who can authenticate through a configured identity provider that asserts a victim email as unverified can have a fresh provider identity merged into the victim Budibase account and inherit the victim roles. This issue is fixed in version 3.39.30.","severity":{"version":"4.0","score":9,"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","level":"critical","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-09","score":0.00427,"percentile":0.37262,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":false,"fix_available":false},"cwes":["CWE-287"],"published":"2026-07-24T21:17:39Z","modified":"2026-10-28T02:30:34Z","affected":[{"source":"ghsa","ecosystem":"npm","package":"@budibase/server","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"last_affected":"3.38.1"}]}]},{"source":"osv","ecosystem":"npm","package":"@budibase/server","ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"last_affected":"3.38.1"}]}]}],"products":[{"vendor":"budibase","product":"budibase"}],"references":[{"url":"https://github.com/Budibase/budibase/security/advisories/GHSA-hp6v-6jw7-gv2f","tags":["x_refsource_CONFIRM","exploit","advisory","web"]},{"url":"https://github.com/Budibase/budibase/pull/19173","tags":["x_refsource_MISC","fix"]},{"url":"https://github.com/Budibase/budibase/commit/9ecd0048d9c3ae0ee9bd0e6204c621794dd1a4d3","tags":["x_refsource_MISC","fix","web"]},{"url":"https://github.com/Budibase/budibase/releases/tag/3.39.30","tags":["x_refsource_MISC","web"]},{"url":"https://github.com/advisories/GHSA-hp6v-6jw7-gv2f"},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/73xxx/CVE-2026-73302.json","tags":["advisory"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-73302","tags":["advisory"]},{"url":"https://github.com/Budibase/budibase","tags":["package"]}],"sources":{"cve":[{"id":"CVE-2026-73302","title":"Budibase: OIDC SSO account takeover: incoming identity linked by email without checking email_verified","description":"Budibase is an open-source low-code platform. Prior to 3.39.30, the OIDC flow in packages/backend-core/src/middleware/passport/sso/oidc.ts resolved an email without getEmailVerified or an email_verified requirement, and packages/backend-core/src/middleware/passport/sso/sso.ts then used users.getGlobalUserByEmail as a fallback account-linking key. An attacker who can authenticate through a configured identity provider that asserts a victim email as unverified can have a fresh provider identity merged into the victim Budibase account and inherit the victim roles. This issue is fixed in version 3.39.30.","status":"published","published":"2026-08-13T22:04:50.179Z","modified":"2026-08-14T16:10:42.639Z","cvss":[{"version":"4.0","score":9,"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","level":"critical","assessor":"cna"}],"cwes":["CWE-287"],"updated_at":"2026-11-02T13:43:22.599321Z"}],"euvd":[{"id":"EUVD-2026-58525","description":"Budibase is an open-source low-code platform. Prior to 3.39.30, the OIDC flow in packages/backend-core/src/middleware/passport/sso/oidc.ts resolved an email without getEmailVerified or an email_verified requirement, and packages/backend-core/src/middleware/passport/sso/sso.ts then used users.getGlobalUserByEmail as a fallback account-linking key. An attacker who can authenticate through a configured identity provider that asserts a victim email as unverified can have a fresh provider identity merged into the victim Budibase account and inherit the victim roles. This issue is fixed in version 3.39.30.","status":"published","published":"2026-08-13T22:04:50Z","modified":"2026-08-14T16:10:42Z","cvss":[{"version":"4.0","score":9,"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","level":"critical"}],"updated_at":"2026-11-02T18:01:54.910263Z"}],"ghsa":[{"id":"GHSA-hp6v-6jw7-gv2f","title":" Budibase: OIDC SSO account takeover: incoming identity linked by email without checking email_verified","description":"### Summary\nBudibase'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 = \u003cvictim\u003e` 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.\n\n### Details\nThe OIDC verify callback extracts the email and never consults `email_verified`:\n- `packages/backend-core/src/middleware/passport/sso/oidc.ts:59` — `email: getEmail(profile, jwtClaims)`.\n- `getEmail` (`oidc.ts:113-135`) returns `profile._json.email` -\u003e`jwtClaims.email` -\u003e `preferred_username`. **No `email_verified` check.**\n- `buildJwtClaims` (`oidc.ts:99-107`) assembles claims from `_json.email`/`emails[0].value` — no verification flag is read. `grep -r email_verified packages/` -\u003e 0 hits.\n\nThe email is then used as the **account-linking key**:\n- `sso.authenticate(...)` -\u003e `packages/backend-core/src/middleware/passport/sso/sso.ts`:\n  - `: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`).\n  - `:57-59` **fallback:** `dbUser = await users.getGlobalUserByEmail(details.email)` -\u003e 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`).\n  - `syncUser(...)` (`sso.ts:80,102-138`) spreads `...user`, preserving the victim `_id`/`tenantId`/`roles`; only overwrites provider fields.\n- `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.\n\nPer 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.\n\n**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:\n- **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 = \u003cvictim\u003e` at sign-up. No confirmation email is needed — the IdP stores and asserts it unverified.**\n- **Self-service profile editing** — many IdPs let a logged-in user change their own email without forced re-verification.\n- **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.\nIt 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.\n\n### PoC\nReproduced 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.\n\n**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.\n\n**Step 1 — the IdP asserts the claim (proves `email_verified=false`):**\n```\nPOST /realms/budi/protocol/openid-connect/token   (Keycloak)\ngrant_type=password\u0026client_id=budibase\u0026client_secret=…\u0026username=attacker\u0026password=Attacker123!\u0026scope=openid email profile\n-\u003e id_token payload: { \"sub\":\"3cf58c45-…\", \"preferred_username\":\"attacker\",\n                      \"email\":\"victim@stand.local\", \"email_verified\":false }\n```\nThe authenticated principal is provably **`attacker`** (its own `sub`/`preferred_username`/password), merely *claiming* the victim's email, unverified.\n\n\u003cimg width=\"1484\" height=\"685\" alt=\"image\" src=\"https://github.com/user-attachments/assets/cdddac3c-b7c0-4c59-aaec-9092706a7ad8\" /\u003e\n\n\n**Step 2 — drive the standard OIDC flow** as `attacker`:\n`GET /api/global/auth/default/oidc/configs/kc-oidc-1` -\u003e IdP login as `attacker`/`Attacker123!` -\u003e `GET /api/global/auth/oidc/callback?code=…\u0026state=…`.\n\n\u003cimg width=\"1145\" height=\"454\" alt=\"image\" src=\"https://github.com/user-attachments/assets/ac0a73d0-c5fc-4d0f-b9b0-95f6273b99f7\" /\u003e\n\n\u003cimg width=\"1172\" height=\"445\" alt=\"image\" src=\"https://github.com/user-attachments/assets/a4b27a7e-4293-4caf-957f-d27c54ea461a\" /\u003e\n\n\u003cimg width=\"1157\" height=\"648\" alt=\"image\" src=\"https://github.com/user-attachments/assets/06699774-92b0-4163-a171-9a30bc877ecc\" /\u003e\n\n\u003cimg width=\"1201\" height=\"525\" alt=\"image\" src=\"https://github.com/user-attachments/assets/92a92fcf-9d29-42ca-921d-de6fbd78198f\" /\u003e\n\n\n\n**Step 3 — result (takeover):** Budibase sets `budibase:auth` to a session JWT\n`{ \"userId\":\"us_1ab2dfcf…\", \"email\":\"victim@stand.local\", \"tenantId\":\"default\" }`, and\n`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.\n\n\u003cimg width=\"1001\" height=\"456\" alt=\"image\" src=\"https://github.com/user-attachments/assets/c8dfc25f-aed7-4cd7-9323-f029e4d1a725\" /\u003e\n\n\n**Negative control (proves the email claim is the cause, not a normal self-login):**\nA second attacker `attacker2` with a **benign** unverified email `attacker2@evil.local` (matching no Budibase user) runs the *identical* flow:\n```\nid_token: { \"sub\":\"55794bf2-…\", \"preferred_username\":\"attacker2\", \"email\":\"attacker2@evil.local\", \"email_verified\":false }\n-\u003e budibase:auth: { \"userId\":\"us_55794bf2-…\" }   (a NEW account, _id derived from the IdP sub)\n-\u003e /api/global/self: { \"_id\":\"us_55794bf2-…\", \"email\":\"attacker2@evil.local\", admin.global: null, builder.global: null }\n```\n\u003cimg width=\"1153\" height=\"489\" alt=\"image\" src=\"https://github.com/user-attachments/assets/8f6cb849-e6f3-49f1-b576-8aeba026a3be\" /\u003e\n\n\u003cimg width=\"1089\" height=\"466\" alt=\"image\" src=\"https://github.com/user-attachments/assets/a892ec3e-3753-4bc0-9eee-9f0613b2d92e\" /\u003e\n\n\u003cimg width=\"826\" height=\"532\" alt=\"image\" src=\"https://github.com/user-attachments/assets/cc30bd9c-1bdb-4bea-934f-a8b3e228940e\" /\u003e\n\n\nWith 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.\n\n### Impact\nTakeover of any existing Budibase account by email, including the instance owner / global admin -\u003e full control of the tenant (apps, datasources, automations, user management, stored datasource credentials). The attacker authenticates as their *own* (different) IdP principal and ends up holding the victim's session and roles. The only requirement beyond a trusted IdP login is that the IdP assert the victim's email unverified — the default for a freshly-created Keycloak/Authentik realm and common in social logins (see Preconditions). Any deployment that trusts an OIDC IdP without enforced email verification is exposed; the Budibase-side flaw (ignoring `email_verified`) is unconditional.\n\n### Remediation\n**Primary fix:** in the OIDC verify path, require `email_verified === true` before using `email` to look up / link an existing account; otherwise reject the login (or fall back to `sub`-only matching and never merge into a pre-existing local/SSO account). Concretely, thread the `email_verified` claim through `buildJwtClaims`/`getEmail` (`oidc.ts`) and gate the `getGlobalUserByEmail` fallback in `sso.ts:57-59` on it.\n\n**Audit the whole class:** apply the same `email_verified` (and, for SAML, `EmailVerified`/assertion-signature) gate to every SSO strategy that links by email — OIDC, SAML, and any social provider — not only the Google strategy (which already passes `requireLocalAccount=true`). Email-based account linking anywhere must require a verified email.\n\n**Defense-in-depth for operators who cannot patch immediately:**\n- On the IdP, enable \"Verify Email\" / require verified email before issuing tokens (Keycloak: realm -\u003e Login -\u003e Verify Email = on), and restrict which email domains the IdP will assert.\n- Prefer `sub`-based account mapping over email in the IdP/Budibase mapping config where available.\n- Audit existing accounts for unexpected OIDC links to privileged users; rotate sessions.","status":"published","published":"2026-07-24T21:17:39Z","modified":"2026-08-12T18:57:08Z","cvss":[{"version":"4.0","score":9,"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","level":"critical"}],"cwes":["CWE-287"],"updated_at":"2026-11-02T14:40:58.888637Z"}],"nvd":[{"id":"CVE-2026-73302","description":"Budibase is an open-source low-code platform. Prior to 3.39.30, the OIDC flow in packages/backend-core/src/middleware/passport/sso/oidc.ts resolved an email without getEmailVerified or an email_verified requirement, and packages/backend-core/src/middleware/passport/sso/sso.ts then used users.getGlobalUserByEmail as a fallback account-linking key. An attacker who can authenticate through a configured identity provider that asserts a victim email as unverified can have a fresh provider identity merged into the victim Budibase account and inherit the victim roles. This issue is fixed in version 3.39.30.","status":"published","published":"2026-08-13T22:17:25.353Z","modified":"2026-09-08T20:56:50.52Z","cvss":[{"version":"4.0","score":9,"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/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X","level":"critical","assessor":"secondary"}],"cwes":["CWE-287"],"updated_at":"2026-11-02T14:12:37.557758Z"}],"osv":[{"id":"CVE-2026-73302","title":"Budibase: OIDC SSO account takeover: incoming identity linked by email without checking email_verified","description":"Budibase is an open-source low-code platform. Prior to 3.39.30, the OIDC flow in packages/backend-core/src/middleware/passport/sso/oidc.ts resolved an email without getEmailVerified or an email_verified requirement, and packages/backend-core/src/middleware/passport/sso/sso.ts then used users.getGlobalUserByEmail as a fallback account-linking key. An attacker who can authenticate through a configured identity provider that asserts a victim email as unverified can have a fresh provider identity merged into the victim Budibase account and inherit the victim roles. This issue is fixed in version 3.39.30.","status":"published","published":"2026-08-13T22:04:50.179Z","modified":"2026-10-28T02:30:34.788872213Z","cvss":[{"version":"4.0","score":9,"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","level":"critical"}],"cwes":["CWE-287"],"updated_at":"2026-11-02T15:45:16.365946Z"},{"id":"GHSA-hp6v-6jw7-gv2f","title":" Budibase: OIDC SSO account takeover: incoming identity linked by email without checking email_verified","description":"### Summary\nBudibase'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 = \u003cvictim\u003e` 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.\n\n### Details\nThe OIDC verify callback extracts the email and never consults `email_verified`:\n- `packages/backend-core/src/middleware/passport/sso/oidc.ts:59` — `email: getEmail(profile, jwtClaims)`.\n- `getEmail` (`oidc.ts:113-135`) returns `profile._json.email` -\u003e`jwtClaims.email` -\u003e `preferred_username`. **No `email_verified` check.**\n- `buildJwtClaims` (`oidc.ts:99-107`) assembles claims from `_json.email`/`emails[0].value` — no verification flag is read. `grep -r email_verified packages/` -\u003e 0 hits.\n\nThe email is then used as the **account-linking key**:\n- `sso.authenticate(...)` -\u003e `packages/backend-core/src/middleware/passport/sso/sso.ts`:\n  - `: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`).\n  - `:57-59` **fallback:** `dbUser = await users.getGlobalUserByEmail(details.email)` -\u003e 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`).\n  - `syncUser(...)` (`sso.ts:80,102-138`) spreads `...user`, preserving the victim `_id`/`tenantId`/`roles`; only overwrites provider fields.\n- `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.\n\nPer 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.\n\n**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:\n- **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 = \u003cvictim\u003e` at sign-up. No confirmation email is needed — the IdP stores and asserts it unverified.**\n- **Self-service profile editing** — many IdPs let a logged-in user change their own email without forced re-verification.\n- **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.\nIt 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.\n\n### PoC\nReproduced 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.\n\n**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.\n\n**Step 1 — the IdP asserts the claim (proves `email_verified=false`):**\n```\nPOST /realms/budi/protocol/openid-connect/token   (Keycloak)\ngrant_type=password\u0026client_id=budibase\u0026client_secret=…\u0026username=attacker\u0026password=Attacker123!\u0026scope=openid email profile\n-\u003e id_token payload: { \"sub\":\"3cf58c45-…\", \"preferred_username\":\"attacker\",\n                      \"email\":\"victim@stand.local\", \"email_verified\":false }\n```\nThe authenticated principal is provably **`attacker`** (its own `sub`/`preferred_username`/password), merely *claiming* the victim's email, unverified.\n\n\u003cimg width=\"1484\" height=\"685\" alt=\"image\" src=\"https://github.com/user-attachments/assets/cdddac3c-b7c0-4c59-aaec-9092706a7ad8\" /\u003e\n\n\n**Step 2 — drive the standard OIDC flow** as `attacker`:\n`GET /api/global/auth/default/oidc/configs/kc-oidc-1` -\u003e IdP login as `attacker`/`Attacker123!` -\u003e `GET /api/global/auth/oidc/callback?code=…\u0026state=…`.\n\n\u003cimg width=\"1145\" height=\"454\" alt=\"image\" src=\"https://github.com/user-attachments/assets/ac0a73d0-c5fc-4d0f-b9b0-95f6273b99f7\" /\u003e\n\n\u003cimg width=\"1172\" height=\"445\" alt=\"image\" src=\"https://github.com/user-attachments/assets/a4b27a7e-4293-4caf-957f-d27c54ea461a\" /\u003e\n\n\u003cimg width=\"1157\" height=\"648\" alt=\"image\" src=\"https://github.com/user-attachments/assets/06699774-92b0-4163-a171-9a30bc877ecc\" /\u003e\n\n\u003cimg width=\"1201\" height=\"525\" alt=\"image\" src=\"https://github.com/user-attachments/assets/92a92fcf-9d29-42ca-921d-de6fbd78198f\" /\u003e\n\n\n\n**Step 3 — result (takeover):** Budibase sets `budibase:auth` to a session JWT\n`{ \"userId\":\"us_1ab2dfcf…\", \"email\":\"victim@stand.local\", \"tenantId\":\"default\" }`, and\n`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.\n\n\u003cimg width=\"1001\" height=\"456\" alt=\"image\" src=\"https://github.com/user-attachments/assets/c8dfc25f-aed7-4cd7-9323-f029e4d1a725\" /\u003e\n\n\n**Negative control (proves the email claim is the cause, not a normal self-login):**\nA second attacker `attacker2` with a **benign** unverified email `attacker2@evil.local` (matching no Budibase user) runs the *identical* flow:\n```\nid_token: { \"sub\":\"55794bf2-…\", \"preferred_username\":\"attacker2\", \"email\":\"attacker2@evil.local\", \"email_verified\":false }\n-\u003e budibase:auth: { \"userId\":\"us_55794bf2-…\" }   (a NEW account, _id derived from the IdP sub)\n-\u003e /api/global/self: { \"_id\":\"us_55794bf2-…\", \"email\":\"attacker2@evil.local\", admin.global: null, builder.global: null }\n```\n\u003cimg width=\"1153\" height=\"489\" alt=\"image\" src=\"https://github.com/user-attachments/assets/8f6cb849-e6f3-49f1-b576-8aeba026a3be\" /\u003e\n\n\u003cimg width=\"1089\" height=\"466\" alt=\"image\" src=\"https://github.com/user-attachments/assets/a892ec3e-3753-4bc0-9eee-9f0613b2d92e\" /\u003e\n\n\u003cimg width=\"826\" height=\"532\" alt=\"image\" src=\"https://github.com/user-attachments/assets/cc30bd9c-1bdb-4bea-934f-a8b3e228940e\" /\u003e\n\n\nWith 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.\n\n### Impact\nTakeover of any existing Budibase account by email, including the instance owner / global admin -\u003e full control of the tenant (apps, datasources, automations, user management, stored datasource credentials). The attacker authenticates as their *own* (different) IdP principal and ends up holding the victim's session and roles. The only requirement beyond a trusted IdP login is that the IdP assert the victim's email unverified — the default for a freshly-created Keycloak/Authentik realm and common in social logins (see Preconditions). Any deployment that trusts an OIDC IdP without enforced email verification is exposed; the Budibase-side flaw (ignoring `email_verified`) is unconditional.\n\n### Remediation\n**Primary fix:** in the OIDC verify path, require `email_verified === true` before using `email` to look up / link an existing account; otherwise reject the login (or fall back to `sub`-only matching and never merge into a pre-existing local/SSO account). Concretely, thread the `email_verified` claim through `buildJwtClaims`/`getEmail` (`oidc.ts`) and gate the `getGlobalUserByEmail` fallback in `sso.ts:57-59` on it.\n\n**Audit the whole class:** apply the same `email_verified` (and, for SAML, `EmailVerified`/assertion-signature) gate to every SSO strategy that links by email — OIDC, SAML, and any social provider — not only the Google strategy (which already passes `requireLocalAccount=true`). Email-based account linking anywhere must require a verified email.\n\n**Defense-in-depth for operators who cannot patch immediately:**\n- On the IdP, enable \"Verify Email\" / require verified email before issuing tokens (Keycloak: realm -\u003e Login -\u003e Verify Email = on), and restrict which email domains the IdP will assert.\n- Prefer `sub`-based account mapping over email in the IdP/Budibase mapping config where available.\n- Audit existing accounts for unexpected OIDC links to privileged users; rotate sessions.","status":"published","published":"2026-07-24T21:17:39Z","modified":"2026-08-12T19:11:12.967676Z","cvss":[{"version":"4.0","score":9,"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","level":"critical"}],"cwes":["CWE-287"],"updated_at":"2026-11-02T15:46:49.28217Z"}]}}
