Repository navigation
chore(deps): update dependency better-auth to v1.7.7 [security] - #87
Open
renovate[bot] wants to merge 1 commit into
Open
renovate[bot] wants to merge 1 commit into
renovate[bot] wants to merge 1 commit into
Conversation
Owner
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
renovate
Bot
force-pushed
the
renovate/npm-better-auth-vulnerability
branch
from
October 30, 2026 07:17
df4c480 to
4a14471
Compare
renovate
Bot
force-pushed
the
renovate/npm-better-auth-vulnerability
branch
from
November 4, 2026 05:39
4a14471 to
7c9b448
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Note
This PR body was truncated due to platform limits.
This PR contains the following updates:
1.6.11→1.7.7Better Auth has stored XSS in the auth-server origin via javascript: redirect_uri in oidc-provider and mcp
GHSA-86j7-9j95-vpqj
More information
Details
Am I affected?
Check each condition. Users are affected when all of the first three hold.
oidc-providerplugin or themcpplugin frombetter-auth/plugins. Themcpplugin wraps the same provider and carries the same defect. Both are on the migration path to@better-auth/oauth-provider, which is not affected.better-authversion is1.6.12or earlier on the stable line, or any1.7.0-betabuild on the pre-release line. Both release lines carry the same defect.redirectURIfrom theauthClient.oauth2.consent(...)response and assigns it to a browser navigation target such aswindow.location.href,location.assign, orlocation.replace.Two conditions raise an application's exposure:
allowDynamicClientRegistration: true. Client registration is then unauthenticated, so any visitor can plant the malicious client. The default isfalse, which limits planting to authenticated users.A developer's application is not affected when any of these hold:
consent-buttons.tsxdemo verbatim. That file reads aurifield that this plugin never returns, so its navigation branch never runs and it falls through to an error toast.javascript:URL delivered in aLocationresponse header.@better-auth/oauth-providerinstead of the deprecatedoidc-providerplugin.Fix:
better-auth@1.6.13(stable) or1.7.0-beta.4(pre-release).Summary
The deprecated
oidc-providerplugin registers OAuth clients without validating the scheme of theirredirect_uris. An attacker stores ajavascript:URI as a client redirect target, and the authorization server later returns that URI to the browser in the consent response. A consent page that navigates to the returned value then executes attacker JavaScript in the authorization-server origin, which exposes the victim's session and enables account takeover. Themcpplugin wraps the same provider and carries the same defect, so MCP server deployments are affected as well; likeoidc-provider, it is migrating to@better-auth/oauth-provider.Details
Client registration accepts any string. The registration body schema types
redirect_urisasz.array(z.string())with no scheme check, soPOST /oauth2/registerstores a value such asjavascript:fetch('/api/auth/get-session')//. In the default configuration,allowDynamicClientRegistrationisfalse, so registration requires an authenticated session; any logged-in user qualifies. With dynamic client registration enabled, registration is unauthenticated.The dangerous value then survives the authorization-code flow unchanged.
GET /oauth2/authorizematches the requestredirect_uriagainst the stored list by exact string equality, so the registeredjavascript:URI matches itself and is written into the consent verification record. When the user approves on the consent screen, the handler runsnew URL(value.redirectURI), appends the authorization code throughsearchParams.set, and returns the result in JSON under the keyredirectURI. Thejavascript:scheme passes throughnew URLintact, and a trailing//in the payload comments out the appended query string.The plugin documents the consent call but not the redirect step that follows it, and it gives no warning that
redirectURIcan carry a dangerous scheme. The natural way an operator completes the flow is to assignres.data.redirectURItowindow.location.href. Per the URL specification and browser behavior, navigatingwindow.locationto ajavascript:URL executes the script body in the current document origin, which here is the authorization server. The injected script can call/api/auth/get-sessionand any other session-scoped endpoint in that origin.The sibling
@better-auth/oauth-providerpackage already prevents this. It validates the same field withSafeUrlSchema, which rejects thejavascript:,data:, andvbscript:schemes and requires HTTPS or a loopback host. The deprecated plugin never received that control; the field shipped unvalidated when the registration endpoint was first added, well before the sibling package introducedSafeUrlSchema.Patches
Fixed in
better-auth@1.6.13(stable) and1.7.0-beta.4(pre-release). The fix adds scheme validation to theredirect_urisof both theoidc-providerandmcpplugins, matching the control@better-auth/oauth-provideralready enforces.Workarounds
If developers cannot upgrade, apply one of the following.
http:orhttps:. For example, reject the value whennew URL(redirectURI).protocolis neitherhttp:norhttps:, and show an error instead of navigating. This closes the sink regardless of what the server returns.@better-auth/oauth-provider. It validates redirect URIs at registration and is the supported replacement for the deprecated plugin.allowDynamicClientRegistrationat its default offalseand restrict who can register clients. This does not remove the issue, because any authenticated user can still register a malicious client, but it removes the unauthenticated path.Impact
This is a stored DOM cross-site scripting issue in the authorization server's own origin, which leads to account takeover. An attacker who can register a client plants a
javascript:redirect URI. A victim who visits the attacker's authorize URL and approves consent runs attacker JavaScript in the authorization-server origin. From there the script reads/api/auth/get-session, calls any session-scoped endpoint, and takes over the account. The consent screen shows the attacker-controlled client name, which makes the approval click easy to socially engineer.The realized impact depends on one operator-side precondition: the consent page must assign the returned
redirectURIto a navigation target. Better Auth itself never performs that navigation, which is why the assessed Attack Complexity is High and the score sits below a pure server-side execution flaw. The deprecated status of theoidc-providerplugin does not reduce the impact. The plugin remains published, importable, and documented, and users on the affected versions cannot opt out without changing their integration. Themcpplugin wraps the same provider and is affected on the same versions; its docs announce a move to@better-auth/oauth-provider, but until users migrate or upgrade, the affected published versions stay exposed.@better-auth/oauth-providervalidates redirect URIs and provides MCP support, so it is the migration target for both plugins.Credit
Reported by @hillalee
Resources
window.locationto ajavascript:URL executes the script body. https://developer.mozilla.org/en-US/docs/Web/URI/Reference/Schemes/javascriptSeverity
CVSS:3.1/AV:N/AC:H/PR:L/UI:R/S:C/C:H/I:H/A:NReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
Better Auth: Account takeover via pre-account hijacking on magic-link and email-OTP sign-in
GHSA-qq9h-g4jm-xgf3
More information
Details
Am I affected
You are affected if all of the following hold:
better-authversion below 1.6.22, or a1.7.0-betabelow1.7.0-beta.10.Summary
An attacker can keep password access to a victim's account after the victim starts using it. The attack runs in three steps. First, with open registration, the attacker signs up using the victim's email and a password the attacker picks. The account stays unverified, so the attacker cannot use it yet. Later the real owner signs in with a magic link or an email OTP. That step marks the account verified, and the attacker's password now works on it.
Details
When an account already exists for an address, magic-link verification and email-OTP sign-in both sign in to that account. They mark it verified and issue a session. Before the fix, neither one removed a password set while the account was still unverified. Neither one revoked existing sessions. So a password set before anyone proved control of the mailbox kept working after the owner proved control.
This is a pre-account-hijacking weakness. It is the same class as the OAuth advisory GHSA-g38m-r43w-p2q7, applied to the passwordless email flows. Requiring email verification does not stop it. The verification step is the exact moment that turns the planted account into a usable one.
The fix treats current proof of control over the address as authoritative. When either flow finds an account whose email was never confirmed, it now removes the password and revokes existing sessions first. Only then does it mark the account verified and issue the new session. The owner signs in as before. Only access created before the proof is removed.
Patches
Upgrade to
better-auth1.6.22 or later on the stable line. On the 1.7 pre-release line, upgrade to1.7.0-beta.10or later. Earlier 1.7 betas, up to and including1.7.0-beta.9, still contain the bug.Workarounds
If you cannot upgrade right away, you can lower the risk in two ways. Require email verification before you accept any password on an account. Remove unverified accounts quickly, so an address cannot sit reserved for long. Both steps shrink the opening, but they do not close it. Upgrading is the real fix.
Impact
The attacker gains lasting password access to an account the victim depends on, alongside the victim. From there the attacker can read and change the victim's data. The attacker can also reset the credentials and lock the victim out. The takeover only completes when the victim signs in through the passwordless flow. It also needs a setup that pairs that flow with open email and password sign-up.
Credit
Reported by the Vercel security team.
Severity
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:LReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
better-auth: Device login approval does not show the requesting client or permissions
GHSA-q84f-53jg-9ppm
More information
Details
Am I affected?
You may be affected if you use
better-authfrom1.3.8-beta.3up to, but not including,1.7.0-rc.3, and you enable thedeviceAuthorization()plugin. This range includes all 1.6 releases.You are affected only if all of these are also true:
validateClientis not configured, or the attacker can use a valid public client.You are not affected if you do not enable the opt-in
deviceAuthorization()plugin.Summary
The device authorization plugin lets a user sign in on one device by approving a short code on another. In affected versions, the approval information given to the user leaves out which client asked for access and which permissions it wants. The official approval examples therefore show only a generic request and a code, so users cannot tell their own device from an attacker's request.
Impact
An attacker who convinces a signed-in user to approve the attacker's code receives a live Better Auth session token for that user. The attacker can then read or change any data the user's account can access. The attack needs the opt-in device flow, a request the attacker creates, and a user who actively approves it; it is not a sign-in bypass without user action.
Patches
The fix first shipped in
1.7.0-rc.3. Upgrade tobetter-auth@1.7.0or later. No 1.6 release contains the fix.After upgrading,
GET /devicereturns the requestingclient_idandscopeto the signed-in account that owns the request. If you built your own approval page, update it to show that client and those permissions, and keep approval disabled until they load.Workarounds
If you cannot upgrade yet, do the following:
deviceAuthorization(), or block untrusted access to/device/code.validateClientallowlist. Do not rely on the allowlist alone: it limits who can start the flow, but it does not give users the missing approval information.Technical details
A device starts the flow by calling
/device/code. The plugin stores the client identifier and any requested scope with the pending request. Before approval,GET /devicereturns only the user code and its status. This is true even for a signed-in session of the user account that owns the request. An approval page built from that response cannot show which client asked for access or which permissions it wants. After approval, the original device code can be redeemed at/device/tokenfor a Better Auth session token.RFC 8628 describes this remote phishing scenario. It recommends that the server tell users they are authorizing a device and ask them to confirm that the device is in their possession. It also recommends showing device information that helps users detect impersonation. Client allowlisting reduces who can start the flow, but it does not give users the missing approval information. A valid public client can therefore still be affected when its identifier and requested permissions are not shown.
Severity
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:NReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
better-auth: OAuth Proxy lets an attacker sign in as another user
GHSA-r4xp-prcw-77qf
More information
Details
Am I affected?
You may be affected if you use
better-authfrom 1.5.0-beta.12 through versions before 1.7.7 with the OAuth Proxy plugin. You are affected only if all of these are also true:secret, or itssecretequals that environment's global Better Auth secret.account.storeStateStrategy: "cookie"). With a database or secondary storage, this is only the case if you chose it explicitly.You are not affected by this path if every participating environment uses a proxy
secretdistinct from its global Better Auth secret, if sign-in state is stored in the database, or if you use a version before 1.5.0-beta.12.Summary
The OAuth Proxy plugin trusts any user profile it can decrypt. In the affected setup, Better Auth encrypts sign-in state, including data the caller chose, with the same secret and hands it to anyone who starts a social sign-in. An attacker can use this to get a fake profile for another user's email address that the proxy accepts as real.
Impact
An attacker without an account who knows a user's verified email address can sign in as that user, with that user's permissions. They can read private data, change the account, and link a provider account of their choice to it. They need no provider sign-in and no action from the victim. No availability impact is established, and a reported open redirect was not confirmed.
Patches
The fix is merged for the planned
better-auth1.7.7 release. That package is not available yet.Upgrade all nodes handling the same cookie-backed OAuth or SAML state flow and all OAuth Proxy participants together, including the production coordinator and preview or development deployments. Keep installed Better Auth packages released alongside
better-authon the same release version. Restart pending sign-in and account-linking flows; pre-upgrade state cookies and proxy payloads are rejected. Public options and API signatures are unchanged.Workarounds
Until you upgrade to the fixed release, apply one of these:
secretacross every environment that takes part in the proxy flow, and keep it different from each environment's global Better Auth secret.account.storeStateStrategy: "database". This is already the default when a database or secondary storage is configured. If Magic Link is also enabled, switching can expose a separate cross-purpose verification-record path in affected releases. Ensure the two flows resolve to distinct stored identifiers; with built-in options, use Magic LinkstoreToken: "hashed"with globalverification.storeIdentifierunset or"plain", or disable Magic Link or both built-in social and Generic OAuth sign-in.Setting
account.accountLinking.disableImplicitLinking: truestops this path from taking over an existing account. It does not fix the underlying flaw: the callback can still accept attacker-made data as a provider identity. Apply one of the steps above as well.Severity
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:NReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
better-auth: OAuth state can be used as a Magic Link to sign in as another user
CVE-2026-114607 / GHSA-965c-763c-88jm
More information
Details
Am I affected?
You may be affected if you use
better-authfrom1.4.0-beta.18through versions before1.7.7with the Magic Link plugin and social or Generic OAuth sign-in.You are affected if all of these are true:
storeToken: "plain", or withstoreToken: "hashed"and globalverification.storeIdentifier: "hashed"(including a hashed default).account.storeStateStrategy: "database", the default when a database or secondary storage is configured.Cookie-backed state blocks this path, subject to the OAuth Proxy condition below. With custom hashers, confirm the flows cannot resolve to the same stored identifier.
Summary
Better Auth accepts an OAuth sign-in state value as a Magic Link token. An attacker can start an OAuth sign-in with any email address and use the returned state value to finish a Magic Link sign-in for that address. They never prove that they can read that email.
Impact
An unauthenticated attacker who knows a user's email address can get a normal session for that user, with all of that account's permissions. They need no access to the mailbox, and they do not need to sign in with the OAuth provider or complete an OAuth callback.
If Magic Link sign-up is enabled and no user has the email address, the attacker can instead create an account that Better Auth wrongly marks as email verified, and is signed in to it. The false verified-email flag can also bypass Better Auth's default unproven-account protections when the mailbox owner later signs in, leaving the attacker's session in that account. Applications that trust verified email addresses or email domains may give this account more access, depending on the application.
Patches
The fix is merged for the planned
better-auth1.7.7 release. That package is not available yet.Upgrade
better-authand installed Better Auth adapters, plugins, and integrations released alongside it together. Upgrade servers sharing verification storage in the same cutover. Request new Magic Links and restart pending OAuth or SAML sign-ins; links and state created before the upgrade cannot complete. Update customverification.storeIdentifier.overridesrules to match themagic-link:andauth-state:prefixes. There is no user or account data migration, and public token values and API signatures are unchanged.Workarounds
Until you upgrade to the fixed release, use one of these mitigations:
account.storeStateStrategyto"cookie". If you also use OAuth Proxy, first configure one dedicated OAuth Proxysecretacross all participating environments that differs from each environment's global Better Auth secret. Cookie-backed state can expose a separate OAuth Proxy account-takeover path when the proxy shares the global secret and implicit account linking is enabled.storeTokento"hashed"and leave globalverification.storeIdentifierunset or set it to"plain". Global identifier hashing does not prevent this issue when Magic Link tokens are also hashed.Setting
disableSignUp: trueon the Magic Link plugin is not enough. It only stops the creation of new verified accounts. It does not stop the takeover of existing accounts.Severity
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:NReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
Better Auth has stored XSS in the auth-server origin via javascript: redirect_uri in oidc-provider and mcp
CVE-2026-67333 / GHSA-86j7-9j95-vpqj
More information
Details
Am I affected?
Check each condition. Users are affected when all of the first three hold.
oidc-providerplugin or themcpplugin frombetter-auth/plugins. Themcpplugin wraps the same provider and carries the same defect. Both are on the migration path to@better-auth/oauth-provider, which is not affected.better-authversion is1.6.12or earlier on the stable line, or any1.7.0-betabuild on the pre-release line. Both release lines carry the same defect.redirectURIfrom theauthClient.oauth2.consent(...)response and assigns it to a browser navigation target such aswindow.location.href,location.assign, orlocation.replace.Two conditions raise an application's exposure:
allowDynamicClientRegistration: true. Client registration is then unauthenticated, so any visitor can plant the malicious client. The default isfalse, which limits planting to authenticated users.A developer's application is not affected when any of these hold:
consent-buttons.tsxdemo verbatim. That file reads aurifield that this plugin never returns, so its navigation branch never runs and it falls through to an error toast.javascript:URL delivered in aLocationresponse header.@better-auth/oauth-providerinstead of the deprecatedoidc-providerplugin.Fix:
better-auth@1.6.13(stable) or1.7.0-beta.4(pre-release).Summary
The deprecated
oidc-providerplugin registers OAuth clients without validating the scheme of theirredirect_uris. An attacker stores ajavascript:URI as a client redirect target, and the authorization server later returns that URI to the browser in the consent response. A consent page that navigates to the returned value then executes attacker JavaScript in the authorization-server origin, which exposes the victim's session and enables account takeover. Themcpplugin wraps the same provider and carries the same defect, so MCP server deployments are affected as well; likeoidc-provider, it is migrating to@better-auth/oauth-provider.Details
Client registration accepts any string. The registration body schema types
redirect_urisasz.array(z.string())with no scheme check, soPOST /oauth2/registerstores a value such asjavascript:fetch('/api/auth/get-session')//. In the default configuration,allowDynamicClientRegistrationisfalse, so registration requires an authenticated session; any logged-in user qualifies. With dynamic client registration enabled, registration is unauthenticated.The dangerous value then survives the authorization-code flow unchanged.
GET /oauth2/authorizematches the requestredirect_uriagainst the stored list by exact string equality, so the registeredjavascript:URI matches itself and is written into the consent verification record. When the user approves on the consent screen, the handler runsnew URL(value.redirectURI), appends the authorization code throughsearchParams.set, and returns the result in JSON under the keyredirectURI. Thejavascript:scheme passes throughnew URLintact, and a trailing//in the payload comments out the appended query string.The plugin documents the consent call but not the redirect step that follows it, and it gives no warning that
redirectURIcan carry a dangerous scheme. The natural way an operator completes the flow is to assignres.data.redirectURItowindow.location.href. Per the URL specification and browser behavior, navigatingwindow.locationto ajavascript:URL executes the script body in the current document origin, which here is the authorization server. The injected script can call/api/auth/get-sessionand any other session-scoped endpoint in that origin.The sibling
@better-auth/oauth-providerpackage already prevents this. It validates the same field withSafeUrlSchema, which rejects thejavascript:,data:, andvbscript:schemes and requires HTTPS or a loopback host. The deprecated plugin never received that control; the field shipped unvalidated when the registration endpoint was first added, well before the sibling package introducedSafeUrlSchema.Patches
Fixed in
better-auth@1.6.13(stable) and1.7.0-beta.4(pre-release). The fix adds scheme validation to theredirect_urisof both theoidc-providerandmcpplugins, matching the control@better-auth/oauth-provideralready enforces.Workarounds
If developers cannot upgrade, apply one of the following.
http:orhttps:. For example, reject the value whennew URL(redirectURI).protocolis neitherhttp:norhttps:, and show an error instead of navigating. This closes the sink regardless of what the server returns.@better-auth/oauth-provider. It validates redirect URIs at registration and is the supported replacement for the deprecated plugin.allowDynamicClientRegistrationat its default offalseand restrict who can register clients. This does not remove the issue, because any authenticated user can still register a malicious client, but it removes the unauthenticated path.Impact
This is a stored DOM cross-site scripting issue in the authorization server's own origin, which leads to account takeover. An attacker who can register a client plants a
javascript:redirect URI. A victim who visits the attacker's authorize URL and approves consent runs attacker JavaScript in the authorization-server origin. From there the script reads/api/auth/get-session, calls any session-scoped endpoint, and takes over the account. The consent screen shows the attacker-controlled client name, which makes the approval click easy to socially engineer.The realized impact depends on one operator-side precondition: the consent page must assign the returned
redirectURIto a navigation target. Better Auth itself never performs that navigation, which is why the assessed Attack Complexity is High and the score sits below a pure server-side execution flaw. The deprecated status of theoidc-providerplugin does not reduce the impact. The plugin remains published, importable, and documented, and users on the affected versions cannot opt out without changing their integration. Themcpplugin wraps the same provider and is affected on the same versions; its docs announce a move to@better-auth/oauth-provider, but until users migrate or upgrade, the affected published versions stay exposed.@better-auth/oauth-providervalidates redirect URIs and provides MCP support, so it is the migration target for both plugins.Credit
Reported by @hillalee
Resources
window.locationto ajavascript:URL executes the script body. https://developer.mozilla.org/en-US/docs/Web/URI/Reference/Schemes/javascriptSeverity
CVSS:3.1/AV:N/AC:H/PR:L/UI:R/S:C/C:H/I:H/A:NReferences
This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).
Better Auth: Account takeover via pre-account hijacking on magic-link and email-OTP sign-in
CVE-2026-67327 / GHSA-qq9h-g4jm-xgf3
More information
Details
Am I affected
You are affected if all of the following hold:
better-authversion below 1.6.22, or a1.7.0-betabelow1.7.0-beta.10.Summary
An attacker can keep password access to a victim's account after the victim starts using it. The attack runs in three steps. First, with open registration, the attacker signs up using the victim's email and a password the attacker picks. The account stays unverified, so the attacker cannot use it yet. Later the real owner signs in with a magic link or an email OTP. That step marks the account verified, and the attacker's password now works on it.
Details
When an account already exists for an address, magic-link verification and email-OTP sign-in both sign in to that account. They mark it verified and issue a session. Before the fix, neither one removed a password set while the account was still unverified. Neither one revoked existing sessions. So a password set before anyone proved control of the mailbox kept working after the owner proved control.
This is a pre-account-hijacking weakness. It is the same class as the OAuth advisory GHSA-g38m-r43w-p2q7, applied to the passwordless email flows. Requiring email verification does not stop it. The verification step is the exact moment that turns the planted account into a usable one.
The fix treats current proof of control over the address as authoritative. When either flow finds an account whose email was never confirmed, it now removes the password and revokes existing sessions first. Only then does it mark the account verified and issue the new session. The owner signs in as before. Only access created before the proof is removed.
Patches
Upgrade to
better-auth1.6.22 or later on the stable line. On the 1.7 pre-release line, upgrade to1.7.0-beta.10or later. Earlier 1.7 betas, up to and including1.7.0-beta.9, still contain the bug.Workarounds
If you cannot upgrade right away, you can lower the risk in two ways. Require email verification before you accept any password on an account. Remove unverified accounts quickly, so an address cannot sit reserved for long. Both steps shrink the opening, but they do not close it. Upgrading is the real fix.
Impact
The attacker gains lasting password access to an account the victim depends on, alongside the victim. From there the attacker can read and change the victim's data. The attacker can also reset the credentials and lock the victim out. The takeover only completes when the victim signs in through the passwordless flow. It also needs a setup that pairs that flow with open email and password sign-up.
Credit
Reported by the Vercel security team.
Severity
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:LReferences
This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).
better-auth: Device login approval does not show the requesting client or permissions
CVE-2026-109346 / GHSA-q84f-53jg-9ppm
More information
Details
Am I affected?
You may be affected if you use
better-authfrom1.3.8-beta.3up to, but not including,1.7.0-rc.3, and you enable thedeviceAuthorization()plugin. This range includes all 1.6 releases.You are affected only if all of these are also true:
validateClientis not configured, or the attacker can use a valid public client.You are not affected if you do not enable the opt-in
deviceAuthorization()plugin.Summary
The device authorization plugin lets a user sign in on one device by approving a short code on another. In affected versions, the approval information given to the user leaves out which client asked for access and which permissions it wants. The official approval examples therefore show only a generic request and a code, so users cannot tell their own device from an attacker's request.
Impact
An attacker who convinces a signed-in user to approve the attacker's code receives a live Better Auth session token for that user. The attacker can then read or change any data the user's account can access. The attack needs the opt-in device flow, a request the attacker creates, and a user who actively approves it; it is not a sign-in bypass without user action.
Patches
The fix first shipped in
1.7.0-rc.3. Upgrade tobetter-auth@1.7.0or later. No 1.6 release contains the fix.After upgrading,
GET /devicereturns the requestingclient_idandscopeto the signed-in account that owns the request. If you built your own approval page, update it to show that client and those permissions, and keep approval disabled until they load.Workarounds
If you cannot upgrade yet, do the following:
deviceAuthorization(), or block untrusted access to/device/code.validateClientallowlist. Do not rely on the allowlist alone: it limits who can start the flow, but it does not give users the missing approval information.Technical details
A device starts the flow by calling
/device/code. The plugin stores the client identifier and any requested scope with the pending request. Before approval,GET /devicereturns only the user code and its status. This is true even for a signed-in session of the user account that owns the request. An approval page built from that response cannot show which client asked for access or which permissions it wants. After approval, the original device code can be redeemed at/device/tokenfor a Better Auth session token.RFC 8628 describes this remote phishing scenario. It recommends that the server tell users they are authorizing a device and ask them to confirm that the device is in their possession. It also recommends showing device information that helps users detect impersonation. Client allowlisting reduces who can start the flow, but it does not give users the missing approval information. A valid public client can therefore still be affected when its identifier and requested permissions are not shown.
Severity
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:NReferences
This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).
better-auth: OAuth state can be used as a Magic Link to sign in as another user
CVE-2026-110862 / CVE-2026-114607 / GHSA-965c-763c-88jm
More information
Details
Am I affected?
You may be affected if you use
better-authfrom1.4.0-beta.18through versions before1.7.7with the Magic Link plugin and social or Generic OAuth sign-in.You are affected if all of these are true:
storeToken: "plain", or withstoreToken: "hashed"and globalverification.storeIdentifier: "hashed"(including a hashed default).account.storeStateStrategy: "database", the default when a database or secondary storage is configured.Cookie-backed state blocks this path, subject to the OAuth Proxy condition below. With custom hashers, confirm the flows cannot resolve to the same stored identifier.
Summary
Better Auth accepts an OAuth sign-in state value as a Magic Link token. An attacker can start an OAuth sign-in with any email address and use the returned state value to finish a Magic Link sign-in for that address. They never prove that they can read that email.
Impact
An unauthenticated attacker who knows a user's email address can get a normal session for that user, with all of that account's permissions. They need no access to the mailbox, and they do not need to sign in with the OAuth provider or complete an OAuth callback.
If Magic Link sign-up is enabled and no user has the email address, the attacker can instead create an account that Better Auth wrongly marks as email verified, and is signed in to it. The false verified-email flag can also bypass Better Auth's default unproven-account protections when the mailbox owner later signs in, leaving the attacker's session in that account. Applications that trust verified email addresses or email domains may give this account more access, depending on the application.
Patches
The fix is merged for the planned
better-auth1.7.7 release. That package is not available yet.Upgrade
better-authand installed Better Auth adapters, plugins, and integrations released alongside it together. Upgrade servers sharing verification storage in the same cutover. Request new Magic Links and restart pending OAuth or SAML sign-ins; links and state created before the upgrade cannot complete. Update customverification.storeIdentifier.overridesrules to match themagic-link:andauth-state:prefixes. There is no user or account data migration, and public token values and API signatures are unchanged.Workarounds
Until you upgrade to the fixed release, use one of these mitigations:
account.storeStateStrategyto"cookie". If you also use OAuth Proxy, first configure one dedicated OAuth Proxysecretacross all participating environments that differs from each environment's global Better Auth secret. Cookie-backed state can expose a separate OAuth Proxy account-takeover path when the proxy shares the global secret and implicit account linking is enabled.storeTokento"hashed"and leave globalverification.storeIdentifierunset or set it to"plain". Global identifier hashing does not prevent this issue when Magic Link tokens are also hashed.Setting
disableSignUp: trueon the Magic Link plugin is not enough. It only stops the creation of new verified accounts. It does not stop the takeover of existing accounts.Severity
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:NReferences
This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).
better-auth: OAuth Proxy lets an attacker sign in as another user
CVE-2026-110864 / GHSA-r4xp-prcw-77qf
More information
Details
Am I affected?
You may be affected if you use
better-authfrom 1.5.0-beta.12 through versions before 1.7.7 with the OAuth Proxy plugin. You are affected only if all of these are also true:secret, or itssecretequals that environment's global Better Auth secret.account.storeStateStrategy: "cookie"). With a database or secondary storage, this is only the case if you chose it explicitly.You are not affected by this path if every participating environment uses a proxy
secretdistinct from its global Better Auth secret, if sign-in state is stored in the database, or if you use a version before 1.5.0-beta.12.Summary
The OAuth Proxy plugin trusts any user profile it can decrypt. In the affected setup, Better Auth encrypts sign-in state, including data the caller chose, with the same secret and hands it to anyone who starts a social sign-in. An attacker can use this to get a fake profile for another user's email address that the proxy accepts as real.
Impact
An attacker without an account who knows a user's verified email address can sign in as that user, with that user's permissions. They can read private data, change the account, and link a provider account of their choice to it. They need no provider sign-in and no action from the victim. No availability impact is established, and a reported open redirect was not confirmed.
Patches
The fix is merged for the planned
better-auth1.7.7 release. That package is not available yet.Upgrade all nodes handling the same cookie-backed OAuth or SAML state flow and all OAuth Proxy participants together, including the production coordinator and preview or development deployments. Keep installed Better Auth packages released alongside
better-authon the same release version. Restart pending sign-in and account-linking flows; pre-upgrade state cookies and proxy payloads are rejected. Public options and API signatures are unchanged.Workarounds
Until you upgrade to the fixed release, apply one of these:
secretacross every environment that takes part in the proxy flow, and keep it different from each environment's global Better Auth secret.account.storeStateStrategy: "database". This is already the default when a database or secondary storage is configured. If Magic Link is also enabled, switching can expose a separate cross-purpose verification-record path in affected releases. Ensure the two flows resolve to distinct stored identifiers; with built-in options, use Magic LinkstoreToken: "hashed"with globalverification.storeIdentifierunset or"plain", or disable Magic Link or both built-in social and Generic OAuth sign-in.Setting
account.accountLinking.disableImplicitLinking: truestops this path from taking over an existing account. It does not fix the underlying flaw: the callback can still accept attacker-made data as a provider identity. Apply one of the steps above as well.Severity
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:NReferences
This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).
Release Notes
better-auth/better-auth (better-auth)
v1.7.7Compare Source
Patch Changes
4186e36Thanks @bytaesu! - Return CAPTCHA errors with the correct JSON Content-Typ