GHSA-86j7-9j95-vpqj: Better Auth has stored XSS in the auth-server origin via javascript: redirect_uri in oidc-provider and mcp
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-authbetter-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).protocolishttp:orhttps:, and show an error otherwise. - Migrate to
@better-auth/oauth-provider. - Keep
allowDynamicClientRegistrationatfalse. 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-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. - Their application
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. - Their application consent page is a client component that reads
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:
- It sets
allowDynamicClientRegistration: true. Client registration is then unauthenticated, so any visitor can plant the malicious client. The default isfalse, 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.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. - 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 aLocationresponse header. - Their application uses
@better-auth/oauth-providerinstead of the deprecatedoidc-providerplugin.
Fix:
- Upgrade to
[email protected](stable) or1.7.0-beta.4(pre-release). - 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: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. - Migrate to
@better-auth/oauth-provider. It validates redirect URIs at registration and is the supported replacement for the deprecated plugin. - Keep
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 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.locationto ajavascript: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 rejectsjavascript:,data:andvbscript: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
redirectURIto 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 entriesCVE-2026-77361
obsidian-tc : Symlink read-ACL scope bypass via lexical path checks
medium4.7Oct 23, 2026CVE-2026-114126
Storybook dev server vulnerable to code injection from the local network
high8.7Oct 22, 2026CVE-2026-113731
djust: Arbitrary absolute-path file read in the find_handlers_for_template MCP tool
low3.3Oct 21, 2026CVE-2026-114125
Storybook: Missing origin validation in Storybook preview allows code injection from malicious websites
critical9.4Oct 21, 2026CVE-2026-111770
MCP TypeScript server SDK: Shared transport or server instances delivered one HTTP client's messages to another
high7.7Oct 20, 2026GHSA-953h-535j-jg2v
agent-router: MCP session resume does not verify the embedded anti-hijacking subject; default Helm seed makes session IDs forgeable
medium6.8Oct 19, 2026
