NewApp Monitoring is liveSign up free
AgentbeamBeta
AI agent vulnerability · Other MCP servers and tools

GHSA-86j7-9j95-vpqj: Better Auth has stored XSS in the auth-server origin via javascript: redirect_uri in oidc-provider and mcp

highCVSS 3.1 7.7

Better Auth's deprecated oidc-provider plugin, and the mcp plugin that wraps it, accept a `javascript:` URI as a client redirect_uri. A consent page that sends the browser to the returned `redirectURI` then runs attacker script in the auth server's origin, which allows account takeover. Fixed in better-auth 1.6.13 and 1.7.0-beta.4.

Published Jul 7, 2026 · Analysis Oct 25, 2026

What an attacker can do

The redirect_uris field on POST /oauth2/register is typed as z.array(z.string()) with no scheme check, so a value like javascript:fetch('/api/auth/get-session')// is stored. GET /oauth2/authorize matches it by exact string equality. When the user approves, the handler builds a URL with new URL, adds the authorization code, and returns it in JSON as redirectURI. The javascript: scheme survives, and the trailing // comments out the added query string.

If the consent page assigns that value to window.location.href, location.assign or location.replace, the script runs in the auth server origin. It can call /api/auth/get-session and any session-scoped endpoint, so the attacker takes over the victim's account. The victim only has to open the attacker's authorize URL and approve. The consent screen shows the attacker's client name, which helps social engineering.

The attacker needs to register a client. By default any logged-in user can. With allowDynamicClientRegistration: true, any visitor can. The score is held down because Better Auth itself never performs the navigation, so the exposure depends on how your consent page is written.

Who is affected

Affected: better-auth 1.6.12 and earlier on the stable line, and 1.7.0-beta.0 up to before 1.7.0-beta.4, when the oidc-provider or mcp plugin from better-auth/plugins is enabled and your consent page navigates to the returned redirectURI.

Not affected: apps using @better-auth/oauth-provider, apps that finish consent with a server-side redirect (browsers ignore a javascript: URL in a Location header), and apps that copied the demo consent-buttons.tsx unchanged.

npm ls better-auth

better-auth

Ecosystem
npm
Affected
< 1.6.13
Fixed in
1.6.13
Check
npm ls better-auth

better-auth

Ecosystem
npm
Affected
>= 1.7.0-beta.0, < 1.7.0-beta.4
Fixed in
1.7.0-beta.4
Check
npm ls better-auth

How to fix it

Upgrade better-auth to 1.6.13 on the stable line or 1.7.0-beta.4 on the pre-release line.

npm install [email protected]

The fix adds scheme validation to redirect_uris in both plugins, matching what @better-auth/oauth-provider does with SafeUrlSchema. Workarounds from the advisory:

  • Before navigating, check that new URL(redirectURI).protocol is http: or https:, and show an error otherwise.
  • Migrate to @better-auth/oauth-provider.
  • Keep allowDynamicClientRegistration at false. This removes only the unauthenticated path.

How to tell if you were hit

Query your stored OAuth client records for redirect_uris entries that start with javascript:, data: or vbscript:. Check logs of POST /oauth2/register for unexpected clients, especially from unauthenticated callers when dynamic registration is on. Delete any such client. Also review sessions of users who approved consent for an unfamiliar client, and revoke them.

Original advisory

From the GitHub Advisory Database (CC BY 4.0).

Am I affected?

Check each condition. Users are affected when all of the first three hold.

  • Their application enables the oidc-provider plugin or the mcp plugin from better-auth/plugins. The mcp plugin wraps the same provider and carries the same defect. Both are on the migration path to @better-auth/oauth-provider, which is not affected.
  • Their application better-auth version is 1.6.12 or earlier on the stable line, or any 1.7.0-beta build on the pre-release line. Both release lines carry the same defect.
  • Their application consent page is a client component that reads redirectURI from the authClient.oauth2.consent(...) response and assigns it to a browser navigation target such as window.location.href, location.assign, or location.replace.

Two conditions raise an application's exposure:

  • It sets allowDynamicClientRegistration: true. Client registration is then unauthenticated, so any visitor can plant the malicious client. The default is false, which limits planting to authenticated users.
  • It exposes the consent page to ordinary end users rather than to a restricted operator audience.

A developer's application is not affected when any of these hold:

  • Their application copied the project's consent-buttons.tsx demo verbatim. That file reads a uri field that this plugin never returns, so its navigation branch never runs and it falls through to an error toast.
  • Their application completse the consent flow with a server-side redirect, for example a Next.js or Remix server action. Browsers do not follow a javascript: URL delivered in a Location response header.
  • Their application uses @better-auth/oauth-provider instead of the deprecated oidc-provider plugin.

Fix:

  1. Upgrade to [email protected] (stable) or 1.7.0-beta.4 (pre-release).
  2. If developers cannot upgrade their applications, see the workarounds below.

Summary

The deprecated oidc-provider plugin registers OAuth clients without validating the scheme of their redirect_uris. An attacker stores a javascript: 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. The mcp plugin wraps the same provider and carries the same defect, so MCP server deployments are affected as well; like oidc-provider, it is migrating to @better-auth/oauth-provider.

Details

Client registration accepts any string. The registration body schema types redirect_uris as z.array(z.string()) with no scheme check, so POST /oauth2/register stores a value such as javascript:fetch('/api/auth/get-session')//. In the default configuration, allowDynamicClientRegistration is false, 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/authorize matches the request redirect_uri against the stored list by exact string equality, so the registered javascript: URI matches itself and is written into the consent verification record. When the user approves on the consent screen, the handler runs new URL(value.redirectURI), appends the authorization code through searchParams.set, and returns the result in JSON under the key redirectURI. The javascript: scheme passes through new URL intact, 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 redirectURI can carry a dangerous scheme. The natural way an operator completes the flow is to assign res.data.redirectURI to window.location.href. Per the URL specification and browser behavior, navigating window.location to a javascript: URL executes the script body in the current document origin, which here is the authorization server. The injected script can call /api/auth/get-session and any other session-scoped endpoint in that origin.

The sibling @better-auth/oauth-provider package already prevents this. It validates the same field with SafeUrlSchema, which rejects the javascript:, data:, and vbscript: 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 introduced SafeUrlSchema.

Patches

Fixed in [email protected] (stable) and 1.7.0-beta.4 (pre-release). The fix adds scheme validation to the redirect_uris of both the oidc-provider and mcp plugins, matching the control @better-auth/oauth-provider already enforces.

Workarounds

If developers cannot upgrade, apply one of the following.

  • Harden the consent page. Before navigating, parse the returned value and navigate only when its scheme is http: or https:. For example, reject the value when new URL(redirectURI).protocol is neither http: nor https:, and show an error instead of navigating. This closes the sink regardless of what the server returns.
  • Migrate to @better-auth/oauth-provider. It validates redirect URIs at registration and is the supported replacement for the deprecated plugin.
  • Keep allowDynamicClientRegistration at its default of false and 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 redirectURI to 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 the oidc-provider plugin 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. The mcp plugin 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-provider validates redirect URIs and provides MCP support, so it is the migration target for both plugins.

Credit

Reported by @hillalee

Resources

  • CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting'). https://cwe.mitre.org/data/definitions/79.html
  • CWE-601: URL Redirection to Untrusted Site ('Open Redirect'). https://cwe.mitre.org/data/definitions/601.html
  • RFC 6749 (OAuth 2.0) Section 3.1.2, Redirection Endpoint: the redirection endpoint URI must be an absolute URI. https://datatracker.ietf.org/doc/html/rfc6749#section-3.1.2
  • MDN, javascript: URLs: navigating window.location to a javascript: URL executes the script body. https://developer.mozilla.org/en-US/docs/Web/URI/Reference/Schemes/javascript

Credited to hillalee.

Questions

Is my app affected if I use @better-auth/oauth-provider?

No. That package already validates redirect URIs with SafeUrlSchema, which rejects javascript:, data: and vbscript: and requires HTTPS or a loopback host. Only the oidc-provider and mcp plugins are affected.

Does my consent page matter?

Yes. The script runs only if the page assigns the returned redirectURI to a browser navigation target. A server-side redirect is not exploitable this way.

Does this have a CVE?

The data for this entry lists only the GHSA id, GHSA-86j7-9j95-vpqj, and no CVE. The 1.6.13 release notes describe the fix as rejecting invalid redirect_uri schemes in oidc-provider and mcp.

References

How this page is made: the data panel comes from the GitHub Advisory Database, CISA’s KEV catalog, the NVD and FIRST EPSS, and updates twice a day. The analysis is written by an AI model from the sources listed above and checked by automated rules before it is published. Check versions against the vendor's advisory before you act. Methodology

More Other MCP servers and tools vulnerabilities

All Other MCP servers and tools entries