GHSA-86j7-9j95-vpqj: Better auth Redirect_uri Vulnerability

HIGH
CVSS 7.7
CWE-601 CWE-79
Published
Advisory Summary

GHSA-86j7-9j95-vpqj is a HIGH (CVSS 7.7) redirect_uri vulnerability (CWE-601: URL Redirection to Untrusted Site ('Open Redirect')) affecting Better auth libraries in the npm ecosystem. The issue involves redirect_uri in oidc-provider and mcp. Remediation is available by upgrading affected packages to version 1.6.13, 1.7.0-beta.4.

GHSA-86j7-9j95-vpqj at a Glance

CVE Identifier N/A
GHSA Identifier GHSA-86j7-9j95-vpqj
Severity HIGH
CVSS v3.1 Base Score 7.7
CWE Classification CWE-601: URL Redirection to Untrusted Site ('Open Redirect')
Affected Product Better auth
Package Ecosystem npm
Published Date
Last Updated

Security Impact

CVSS: 7.7

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:H/PR:L/UI:R/S:C/C:H/I:H/A:N).

Attack Vector Network (AV:N)
Attack Complexity High (AC:H)
Privileges Required Low (PR:L)
User Interaction Required (UI:R)
Scope Changed (S:C)
Confidentiality High (C:H)
Integrity High (I:H)
Availability None (A:N)

How to Fix GHSA-86j7-9j95-vpqj

To remediate this vulnerability, upgrade all affected Better auth dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

better-auth fixed: 1.6.13
npm install better-auth@1.6.13
better-auth fixed: 1.7.0-beta.4
npm install better-auth@1.7.0-beta.4
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

npm ls better-auth

How to Check if You Are Affected

To determine whether your Node.js application is affected by GHSA-86j7-9j95-vpqj, inspect your project dependencies using npm:

npm ls better-auth

Analyze the terminal output to locate any version of Better auth in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 1.6.13), your application is affected.

Note: The npm ls better-auth command inspects dependencies declared in your package.json and lockfile; it does not perform automated runtime dynamic scanning.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-601: URL Redirection to Untrusted Site ('Open Redirect'), where the web application accepts a user-controlled input that specifies a link to an external site, and uses that link in a redirect. In Better auth, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

### 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 `better-auth@1.6.13` (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 `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. ### 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

GHSA-86j7-9j95-vpqj Affected Better auth Versions

2 component(s)
Fixed Versions:
1.6.131.7.0-beta.4
Product / Package Ecosystem Affected Versions Fixed Version
better-auth
npm
< 1.6.13
1.6.13
better-auth
npm
≥ 1.7.0-beta.0
< 1.7.0-beta.4
1.7.0-beta.4

References

Frequently Asked Questions

What is GHSA-86j7-9j95-vpqj?
GHSA-86j7-9j95-vpqj is a security vulnerability in Better auth involving redirect_uri in oidc-provider and mcp under CWE-601: URL Redirection to Untrusted Site ('Open Redirect').
Which Better auth versions are affected?
Affected versions include releases prior to 1.6.13, 1.7.0-beta.4 across affected packages: better-auth.
What version fixes GHSA-86j7-9j95-vpqj?
Upgrading to 1.6.13, 1.7.0-beta.4 (or the latest patched release for your respective branch) resolves the vulnerability.
Is better-auth affected?
Yes, better-auth is affected in releases prior to 1.6.13, 1.7.0-beta.4. Upgrade your dependency to 1.6.13, 1.7.0-beta.4 or later.
What is the CVSS score of GHSA-86j7-9j95-vpqj?
GHSA-86j7-9j95-vpqj has a CVSS v3.1 base score of 7.7 (HIGH) with vector string CVSS:3.1/AV:N/AC:H/PR:L/UI:R/S:C/C:H/I:H/A:N.
What CWE is associated with GHSA-86j7-9j95-vpqj?
GHSA-86j7-9j95-vpqj is classified under CWE-601: URL Redirection to Untrusted Site ('Open Redirect') (The web application accepts a user-controlled input that specifies a link to an external site, and uses that link in a redirect.).
How can I check npm dependencies for GHSA-86j7-9j95-vpqj?
Run the dependency tree command: 'npm ls better-auth'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-86j7-9j95-vpqj",
  "modified" : "2026-07-07T20:55:28Z",
  "published" : "2026-07-07T20:55:28Z",
  "aliases" : [ ],
  "summary" : "Better Auth has stored XSS in the auth-server origin via javascript: redirect_uri in oidc-provider and mcp",
  "details" : "### 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",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:H/PR:L/UI:R/S:C/C:H/I:H/A:N"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "npm",
      "name" : "better-auth"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "1.6.13"
      } ]
    } ]
  }, {
    "package" : {
      "ecosystem" : "npm",
      "name" : "better-auth"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "1.7.0-beta.0"
      }, {
        "fixed" : "1.7.0-beta.4"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/better-auth/better-auth/security/advisories/GHSA-86j7-9j95-vpqj"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/better-auth/better-auth"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/better-auth/better-auth/releases/tag/v1.6.13"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/better-auth/better-auth/releases/tag/v1.7.0-beta.4"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-601", "CWE-79" ],
    "severity" : "HIGH",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-07-07T20:55:28Z",
    "nvd_published_at" : null
  }
}