Skip to content

chore(deps): update dependency better-auth to v1.6.2 [security]#177

Open
renovate[bot] wants to merge 1 commit into
masterfrom
renovate/npm-better-auth-vulnerability
Open

chore(deps): update dependency better-auth to v1.6.2 [security]#177
renovate[bot] wants to merge 1 commit into
masterfrom
renovate/npm-better-auth-vulnerability

Conversation

@renovate
Copy link
Copy Markdown
Contributor

@renovate renovate Bot commented May 15, 2026

This PR contains the following updates:

Package Change Age Confidence
better-auth (source) 1.5.61.6.2 age confidence

Better Auth: OAuth callback accepts mismatched state when cookie-backed state storage is used without PKCE

GHSA-wxw3-q3m9-c3jr

More information

Details

Am I affected?

Users are affected if all of the following are true:

  • The application uses better-auth at a version below 1.6.2 (or @better-auth/sso paired with such a version).
  • betterAuth({ account: { storeStateStrategy } }) is set to "cookie". The default "database" is not affected.
  • The application wires at least one OAuth provider through genericOAuth({ config }) with pkce: false, or it supplies a custom getToken or tokenUrl that does not require the stored codeVerifier. Stock social providers with PKCE on are not affected.
  • The provider returns arbitrary code values to the configured callback URL.

If users are on better-auth@1.6.2 or later, they are not affected.

Fix:

  1. Upgrade to better-auth@1.6.2 or later (current stable is 1.6.10).
  2. If users cannot upgrade, see workarounds below.
Summary

In parseGenericState, the cookie branch decrypted the oauth_state cookie and validated expiry, but did not compare the incoming OAuth state query parameter to the nonce that generateGenericState issued at sign-in. Any callback to /api/auth/oauth2/callback/<providerId> that arrived with a forged state and any code was therefore accepted as long as the browser still held a live oauth_state cookie. With pkce: false (or any getToken path that does not enforce a code-verifier round-trip), an attacker who forced the victim to deliver an attacker-controlled authorization code to the callback would mint a session bound to the attacker's external identity in the victim's browser. Account-linking flows behaved the same way, binding the attacker's external account to an authenticated victim row.

Details

The cookie branch of parseGenericState did not compare the cookie's stored nonce to the incoming state parameter. The database branch (the default) was not affected because the verification row is keyed by state and the lookup itself enforces equality.

The fix re-binds the cookie to the nonce: generateGenericState writes oauthState: state into the encrypted payload before storage, and parseGenericState rejects when parsedData.oauthState !== state. The same primitive covers every caller (generic-oauth, social, account-link, oauth-proxy passthrough, OIDC SSO, SAML relay state).

Patches

Fixed in better-auth@1.6.2 via PR #​8949 (commit 9deb7936a, merged 2026-04-09). The cookie branch of parseGenericState now rejects when the encrypted payload's nonce does not match the incoming state parameter; the database branch gained a defense-in-depth equality check.

Workarounds

If users cannot upgrade immediately:

  • Switch storeStateStrategy back to "database" (the default). This closes the cookie-only bypass without a code change.
  • Enable pkce: true on every affected genericOAuth provider. The codeVerifier is the missing primitive that the attacker cannot supply.
Impact
  • Forced-login (CSRF on OAuth callback): the attacker forces the victim's browser into an authenticated session bound to the attacker's external identity, allowing the attacker to observe the victim's actions inside the application.
  • Persistent account linking: account-link flows bind the attacker's external account to the victim's authenticated row, granting persistent access until the link is removed.
Credit

Reported by @​Jvr2022 via private advisory disclosure, and by @​alavesa (PatchPilots audit) via the public duplicate issue #​8897.

Resources

Severity

  • CVSS Score: 5.3 / 10 (Medium)
  • Vector String: CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:N/I:H/A:N

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


Release Notes

better-auth/better-auth (better-auth)

v1.6.2

Compare Source

Patch Changes
  • #​8949 9deb793 Thanks @​ping-maxwell! - security: verify OAuth state parameter against cookie-stored nonce to prevent CSRF on cookie-backed flows

  • #​8983 2cbcb9b Thanks @​jaydeep-pipaliya! - fix(oauth2): prevent cross-provider account collision in link-social callback

    The link-social callback used findAccount(accountId) which matched by account ID across all providers. When two providers return the same numeric ID (e.g. both Google and GitHub assign 99999), the lookup could match the wrong provider's account, causing a spurious account_already_linked_to_different_user error or silently updating the wrong account's tokens.

    Replaced with findAccountByProviderId(accountId, providerId) to scope the lookup to the correct provider, matching the pattern already used in the generic OAuth plugin.

  • #​9059 b20fa42 Thanks @​gustavovalverde! - fix(next-js): replace cookie probe with header-based RSC detection in nextCookies() to prevent infinite router refresh loops and eliminate leaked __better-auth-cookie-store cookie. Also fix two-factor enrollment flows to set the new session cookie before deleting the old session.

  • #​9058 608d8c3 Thanks @​gustavovalverde! - fix(sso): include RelayState in signed SAML AuthnRequests per SAML 2.0 Bindings §3.4.4.1

    • RelayState is now passed to samlify's ServiceProvider constructor so it is included in the redirect binding signature. Previously it was appended after the signature, causing spec-compliant IdPs to reject signed AuthnRequests.
    • authnRequestsSigned: true without a private key now throws instead of silently sending unsigned requests.
  • #​8772 8409843 Thanks @​aarmful! - feat(two-factor): include enabled 2fa methods in sign-in redirect response

    The 2FA sign-in redirect now returns twoFactorMethods (e.g. ["totp", "otp"]) so frontends can render the correct verification UI without guessing. The onTwoFactorRedirect client callback receives twoFactorMethods as a context parameter.

    • TOTP is included only when the user has a verified TOTP secret and TOTP is not disabled in config.
    • OTP is included when otpOptions.sendOTP is configured.
    • Unverified TOTP enrollments are excluded from the methods list.
  • #​8711 e78a7b1 Thanks @​aarmful! - fix(two-factor): prevent unverified TOTP enrollment from gating sign-in

    Adds a verified boolean column to the twoFactor table that tracks whether a TOTP secret has been confirmed by the user.

    • First-time enrollment: enableTwoFactor creates the row with verified: false. The row is promoted to verified: true only after verifyTOTP succeeds with a valid code.
    • Re-enrollment (calling enableTwoFactor when TOTP is already verified): the new row preserves verified: true, so the user is never locked out of sign-in while rotating their TOTP secret.
    • Sign-in: verifyTOTP rejects rows where verified === false, preventing abandoned enrollments from blocking authentication. Backup codes and OTP are unaffected and work as fallbacks during unfinished enrollment.

    Migration: The new column defaults to true, so existing twoFactor rows are treated as verified. No data migration is required. skipVerificationOnEnable: true is also unaffected — the row is created as verified: true in that mode.

  • Updated dependencies []:

v1.6.1

Compare Source

Patch Changes

v1.6.0

Compare Source

Minor Changes
Patch Changes

Configuration

📅 Schedule: (in timezone Australia/Melbourne)

  • Branch creation
    • ""
  • Automerge
    • At any time (no schedule defined)

🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.

Rebasing: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 Ignore: Close this PR and you won't be reminded about this update again.


  • If you want to rebase/retry this PR, check this box

This PR was generated by Mend Renovate. View the repository job log.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant