{"id":"GHSA-86j7-9j95-vpqj","title":"Better Auth has stored XSS in the auth-server origin via javascript: redirect_uri in oidc-provider and mcp","summary":"Better Auth has stored XSS in the auth-server origin via javascript: redirect_uri in oidc-provider and mcp","severity":"high","cvss":7.7,"cwe":["CWE-79","CWE-601"],"vendor":"better-auth","product":"better-auth","ecosystem":"npm","affected":["better-auth < 1.6.13","better-auth >= 1.7.0-beta.0, < 1.7.0-beta.4"],"patched":["better-auth 1.6.13","better-auth 1.7.0-beta.4"],"published":"2026-07-07","updated":"2026-07-07","source":"GHSA","sourceUrl":"https://github.com/advisories/GHSA-86j7-9j95-vpqj","references":[{"url":"https://github.com/better-auth/better-auth/security/advisories/GHSA-86j7-9j95-vpqj"},{"url":"https://github.com/better-auth/better-auth/releases/tag/v1.6.13"},{"url":"https://github.com/better-auth/better-auth/releases/tag/v1.7.0-beta.4"},{"url":"https://github.com/advisories/GHSA-86j7-9j95-vpqj"}],"tags":["ghsa","npm"],"ingestedAt":"2026-07-07T21:43:23.247Z","slug":"GHSA-86j7-9j95-vpqj","body":"## Overview\n\n### Am I affected?\n\nCheck each condition. Users are affected when all of the first three hold.\n\n- 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.\n- 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.\n- 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`.\n\nTwo conditions raise an application's exposure:\n\n- 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.\n- It exposes the consent page to ordinary end users rather than to a restricted operator audience.\n\nA developer's application is not affected when any of these hold:\n\n- 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.\n- 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.\n- Their application uses `@better-auth/oauth-provider` instead of the deprecated `oidc-provider` plugin.\n\nFix:\n\n1. Upgrade to `better-auth@1.6.13` (stable) or `1.7.0-beta.4` (pre-release).\n2. If developers cannot upgrade their applications, see the workarounds below.\n\n### Summary\n\nThe 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`.\n\n### Details\n\nClient 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.\n\nThe 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.\n\nThe 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.\n\nThe 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`.\n\n### Patches\n\nFixed in `better-auth@1.6.13` (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.\n\n### Workarounds\n\nIf developers cannot upgrade, apply one of the following.\n\n- 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.\n- Migrate to `@better-auth/oauth-provider`. It validates redirect URIs at registration and is the supported replacement for the deprecated plugin.\n- 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.\n\n### Impact\n\nThis 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.\n\nThe 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.\n\n### Credit\n\nReported by @hillalee \n\n### Resources\n\n- CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting'). https://cwe.mitre.org/data/definitions/79.html\n- CWE-601: URL Redirection to Untrusted Site ('Open Redirect'). https://cwe.mitre.org/data/definitions/601.html\n- 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\n- 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\n\n## Affected packages\n\n- `better-auth < 1.6.13`\n- `better-auth >= 1.7.0-beta.0, < 1.7.0-beta.4`\n\n## Remediation\n\nUpgrade to a patched release:\n\n- `better-auth 1.6.13`\n- `better-auth 1.7.0-beta.4`","depth":"twilight","depthScore":42,"depthScoreParts":{"impact":42.4,"likelihood":0,"exploitation":0,"ransomware":0},"changes":[]}