{"id":"CVE-2026-53516","aliases":["GHSA-g38m-r43w-p2q7"],"title":"Better Auth has an account takeover issue via OAuth auto-link to unverified pre-registered email","summary":"Better Auth has an account takeover issue via OAuth auto-link to unverified pre-registered email","severity":"high","cvss":8.3,"cwe":["CWE-287","CWE-345"],"vendor":"better-auth","product":"better-auth","ecosystem":"npm","affected":["better-auth < 1.6.11"],"patched":["better-auth 1.6.11"],"published":"2026-07-07","updated":"2026-07-07","source":"GHSA","sourceUrl":"https://github.com/advisories/GHSA-g38m-r43w-p2q7","references":[{"url":"https://github.com/better-auth/better-auth/security/advisories/GHSA-g38m-r43w-p2q7"},{"url":"https://github.com/better-auth/better-auth/releases/tag/v1.6.11"},{"url":"https://github.com/advisories/GHSA-g38m-r43w-p2q7"}],"tags":["ghsa","npm"],"ingestedAt":"2026-07-07T21:43:23.250Z","epss":0.00289,"epssPercentile":0.21686,"slug":"CVE-2026-53516","body":"## Overview\n\n### Am I affected?\n\nUsers are affected if all of the following are true:\n\n- Their application uses `better-auth` at a version `< 1.6.11` on the stable line, or any current `next` pre-release.\n- `emailAndPassword.enabled: true` is set in their application's `betterAuth({ ... })` configuration.\n- At least one OAuth or SSO provider is configured (any built-in social provider, or `genericOAuth(...)`, or any provider via `@better-auth/sso`).\n- `account.accountLinking.disableImplicitLinking` is not set to `true`.\n- `account.accountLinking.enabled` is not set to `false`.\n\nSetting either `disableImplicitLinking: true` or `enabled: false` closes the hole at the cost of breaking the standard \"add another login method\" UX. `emailAndPassword.requireEmailVerification: true` does not mitigate, because the link-time `emailVerified` flip promotes the attacker's row to verified, after which the password login becomes usable.\n\nFix:\n\n1. Upgrade to `better-auth@1.6.11` or later.\n2. If developers cannot upgrade, see workarounds below.\n\n### Summary\n\nThe OAuth callback's auto-link gate in `handleOAuthUserInfo` admits an implicit account link whenever the provider asserts `email_verified: true`, without requiring the local user row's `emailVerified` to also be `true`. An attacker who pre-registers a victim's email through `/sign-up/email` (which writes a row with `emailVerified: false`) can have the victim's later OAuth identity bound to the attacker's user row, granting both a password login and the victim's OAuth identity on the same account. This is the pre-account-hijacking class — the same shape as Microsoft \"nOAuth\" (2023) and the Sign in with Apple JWT flaw (2020).\n\n### Details\n\nThe auto-link gate validates only the OAuth provider's `userInfo.emailVerified` claim. The local row's `emailVerified` field is never read. When no `(accountId, providerId)` match exists, the user lookup falls back to email, which surfaces any pre-registered row at that email.\n\nA separate post-link step promotes the local `emailVerified` to `true` when the provider's claim is `true` and the local email matches the provider's email. This step is correct for legitimate first-time linking, but combined with the missing local-side check it becomes load-bearing for the takeover: after the link, the attacker's password row is treated as verified, defeating `requireEmailVerification: true` as a mitigation.\n\nThe fix adds the local-side ownership check to the gate: implicit linking now also rejects when `dbUser.user.emailVerified` is `false`. The same primitive lives in `one-tap` and inherits the same fix shape; the SSO `domainVerified` short-circuit follows separately as a hardening change.\n\n### Patches\n\nFixed in `better-auth@1.6.11`. Implicit linking now refuses to attach an OAuth identity to a local account whose `emailVerified` flag is `false`. The same gate change applies in the `one-tap` sign-in plugin, which previously had its own simpler linking path. The Google ID-token `email_verified` claim is also normalized through `toBoolean` so a string `\"false\"` is treated as falsy (some Google responses send the string, which the prior code treated as truthy).\n\nThe public surface for the new gate is `account.accountLinking.requireLocalEmailVerified`, defaulted to `true`. Applications whose users sign up through OAuth without ever verifying their email locally can opt out with `account: { accountLinking: { requireLocalEmailVerified: false } }` to retain the legacy permissive behavior. The option is marked `@deprecated`; the gate at each call site carries a `FIXME` pointing at the next-minor follow-up that drops the option and makes the check unconditional.\n\nTest fixtures across the `admin`, `oidc-provider`, `mcp`, `generic-oauth`, `last-login-method`, and `oauth-provider` suites now pre-verify created users via a `databaseHooks.user.create.before` hook (or the `disableTestUser` opt-in on the oauth-provider RP fixture) so those suites continue to exercise their role and flow logic rather than tripping the new gate.\n\n### Workarounds\n\nIf developers cannot upgrade their applications immediately:\n\n- **Disable implicit linking**: set `account.accountLinking.disableImplicitLinking: true`. Forces all linking through the authenticated `/link-social` endpoint where the user must already be signed in.\n- **Disable linking entirely**: set `account.accountLinking.enabled: false`. Closes the hole but breaks the multi-login-method UX entirely.\n\n`emailAndPassword.requireEmailVerification: true` alone does not mitigate, because the link-time `emailVerified` flip promotes the attacker's row to verified.\n\n### Impact\n\n- **Account takeover via pre-account hijacking**: the attacker holds a working password login plus the victim's OAuth identity on the same account, granting persistent access.\n- **`requireEmailVerification: true` bypass**: the attacker's password login becomes usable post-link.\n- **Cross-flow reach**: every OAuth and SSO sign-in path that calls `handleOAuthUserInfo` is affected (built-in social providers, generic-oauth, oauth-proxy, SSO OIDC, SSO SAML, one-tap).\n\n### Credit\n\nReported by @avrmeduard.\n\n### Resources\n\n- [CWE-287: Improper Authentication](https://cwe.mitre.org/data/definitions/287.html)\n- [CWE-345: Insufficient Verification of Data Authenticity](https://cwe.mitre.org/data/definitions/345.html)\n- [Sudhodanan & Paverd, Pre-hijacked accounts: an empirical study of security failures in user account creation on the web (USENIX Security 2022)](https://www.usenix.org/conference/usenixsecurity22/presentation/sudhodanan)\n\n## Affected packages\n\n- `better-auth < 1.6.11`\n\n## Remediation\n\nUpgrade to a patched release:\n\n- `better-auth 1.6.11`","depth":"twilight","depthScore":46,"depthScoreParts":{"impact":45.7,"likelihood":0.1,"exploitation":0,"ransomware":0},"changes":[]}