CVE-2026-53512: Better Auth OAuth Refresh-token Replay Vulnerability

CRITICAL
Advisory Identifier: GHSA-pw9m-5jxm-xr6h
CVSS 9.1
CWE-287 CWE-345 CWE-306 CWE-863
Published
Advisory Summary

CVE-2026-53512 is a CRITICAL (CVSS 9.1) oauth refresh-token replay vulnerability (CWE-287: Improper Authentication) affecting Better Auth libraries in the npm ecosystem. The issue involves oauth refresh-token replay via missing client authentication on oidc-provider and mcp plugins. Remediation is available by upgrading affected packages to version 1.6.11.

CVE-2026-53512 at a Glance

CVE Identifier CVE-2026-53512
GHSA Identifier GHSA-pw9m-5jxm-xr6h
Severity CRITICAL
CVSS v3.1 Base Score 9.1
CWE Classification CWE-287: Improper Authentication
Affected Product Better Auth
Package Ecosystem npm
Published Date
Last Updated

Security Impact

CVSS: 9.1

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

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

How to Fix CVE-2026-53512

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.11
npm install better-auth@1.6.11
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 CVE-2026-53512, 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.11), 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-287: Improper Authentication, where when an actor claims to have a given identity, the product does not prove or insufficiently proves that the claim is correct. 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? Users are affected if all of the following are true: - Their application uses `better-auth` and has enabled at least one of: `oidcProvider()` (imported from `better-auth/plugins/oidc-provider`), or `mcp()` (imported from `better-auth/plugins/mcp`). - Their application has at least one confidential OAuth client registered (any client with `type: "web" | "native" | "user-agent-based"` in the `oauthApplication` table, or any `trustedClients` entry without `type: "public"`). Public clients with PKCE are not affected. - Their application uses `better-auth` at a version below the patched release. If an application only uses `@better-auth/oauth-provider` (the canonical replacement for `oidc-provider`) and the `mcp` plugin is not enabled, it is not affected. Fix: 1. Upgrade to `better-auth@1.6.11` or later. 2. Migrate from the deprecated `oidcProvider()` to `@better-auth/oauth-provider` when feasible. The new package enforces client authentication on both grants by default. 3. If developers cannot upgrade their applications, see workarounds below. ### Summary The legacy `oidcProvider` and `mcp` plugins each expose an OAuth 2.0 token endpoint whose `refresh_token` grant authenticates the request entirely on possession of the bound `refreshToken` row and a matching `client_id`. Neither plugin verifies the registered confidential client's `client_secret` on the refresh path. An attacker who obtains any valid `refresh_token` (via database read, log capture, browser-side XSS, or CORS-amplified script in the mcp case) and the public `client_id` can mint fresh access tokens and rotated refresh tokens until the chain is revoked. ### Details RFC 6749 §6 and OAuth 2.1 §4.3 require confidential clients to authenticate to the token endpoint on every grant, including refresh. The same plugins' `authorization_code` grant correctly enforces `client_secret` (the oidc-provider via `verifyStoredClientSecret`, the mcp plugin via raw equality), which proves the omission on the refresh path is a regression rather than a design choice. Token rotation issues a new `refresh_token` with each call, so a single leaked refresh-token grants indefinite access until the row is revoked or its `refreshTokenExpiresAt` (default 7 days) passes; rotation refreshes that window each call. Two adjacent issues on the mcp surface ship in the same patch. The mcp `authorization_code` grant uses raw `===` for client-secret comparison and ignores the `storeClientSecret: "encrypted" | "hashed"` configuration; the fix routes both grants through `verifyStoredClientSecret`. The mcp `/mcp/token` endpoint sets `Access-Control-Allow-Origin: *` unconditionally, which amplifies the refresh bypass in browser contexts; the fix narrows the CORS allowlist. The newer `@better-auth/oauth-provider` package routes both grants through `validateClientCredentials` and is not affected. ### Patches Fixed in `better-auth@1.6.11`. The legacy `oidcProvider` and `mcp` token endpoints now require `client_secret` on the `refresh_token` grant for confidential clients, using the same constant-time comparison the `authorization_code` grant already used. Public clients are unaffected (they have no secret to enforce, and PKCE substitutes on the auth-code grant). The `Authorization: Basic` parser is fixed to follow RFC 6749 §2.3.1: the credential is split on the first colon and each half is percent-decoded. Client IDs and secrets that contain reserved characters now authenticate correctly. The `/mcp/token` endpoint's CORS configuration is narrowed in the same change (the wildcard `Access-Control-Allow-Origin: *` header is removed), matching the standalone `@better-auth/oauth-provider` package. The deprecated `oidc-provider` plugin remains deprecated. The recommended migration path is `@better-auth/oauth-provider`. ### Workarounds None of these close the bug fully without a code patch. - **Migrate to `@better-auth/oauth-provider`** if your deployment can adopt the new plugin. It enforces `client_secret` on both grants. - **Force all clients to public + PKCE**: set every client's `type: "public"` and require PKCE. The bug is unreachable when there is no `client_secret` to verify. - **Network-layer ingress restriction**: limit `/api/auth/oauth2/token` and `/api/auth/mcp/token` to known client IPs at the load balancer. Practical for server-to-server flows, not for end-user-device clients. - **Out-of-band refresh-token rotation**: on any suspicion of leak, run `db.deleteMany({ model: "oauthAccessToken", where: [{ field: "clientId", value: <id> }] })` to invalidate all refresh tokens for the affected client. - **For the mcp endpoint specifically**: drop the wildcard CORS at an upstream proxy and replace with a tight allowlist. ### Impact - **Indefinite confidential-client impersonation**: an attacker holding any valid `refresh_token` and the public `client_id` can mint access tokens and rotated refresh tokens indefinitely, until the row is revoked. Rotation refreshes the expiration window each call. - **Resource access at the user's authorized scope**: every minted access token carries the original user's authorization scope, so the attacker reads or writes whatever the resource server grants for that scope. ### Credit Reported by @subhanUmer. ### Resources - [CWE-306: Missing Authentication for Critical Function](https://cwe.mitre.org/data/definitions/306.html) - [CWE-287: Improper Authentication](https://cwe.mitre.org/data/definitions/287.html) - [CWE-345: Insufficient Verification of Data Authenticity](https://cwe.mitre.org/data/definitions/345.html) - [CWE-863: Incorrect Authorization](https://cwe.mitre.org/data/definitions/863.html) - [RFC 6749 §6: Refreshing an Access Token](https://datatracker.ietf.org/doc/html/rfc6749#section-6) - [OAuth 2.1 §4.3: Refresh Token](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1#section-4.3)

CVE-2026-53512 Affected Better Auth Versions

1 component(s)
Fixed Versions:
1.6.11
Product / Package Ecosystem Affected Versions Fixed Version
better-auth
npm
< 1.6.11
1.6.11

References

Frequently Asked Questions

What is CVE-2026-53512?
CVE-2026-53512 is a security vulnerability in Better Auth involving OAuth refresh-token replay via missing client authentication on oidc-provider and mcp plugins under CWE-287: Improper Authentication.
Which Better Auth versions are affected?
Affected versions include releases prior to 1.6.11 across affected packages: better-auth.
What version fixes CVE-2026-53512?
Upgrading to 1.6.11 (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.11. Upgrade your dependency to 1.6.11 or later.
What is the CVSS score of CVE-2026-53512?
CVE-2026-53512 has a CVSS v3.1 base score of 9.1 (CRITICAL) with vector string CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N.
What CWE is associated with CVE-2026-53512?
CVE-2026-53512 is classified under CWE-287: Improper Authentication (When an actor claims to have a given identity, the product does not prove or insufficiently proves that the claim is correct.).
How can I check npm dependencies for CVE-2026-53512?
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-pw9m-5jxm-xr6h",
  "modified" : "2026-07-20T19:24:31Z",
  "published" : "2026-07-07T20:11:50Z",
  "aliases" : [ "CVE-2026-53512" ],
  "summary" : "Better Auth: OAuth refresh-token replay via missing client authentication on oidc-provider and mcp plugins",
  "details" : "### Am I affected?\n\nUsers are affected if all of the following are true:\n\n- Their application uses `better-auth` and has enabled at least one of: `oidcProvider()` (imported from `better-auth/plugins/oidc-provider`), or `mcp()` (imported from `better-auth/plugins/mcp`).\n- Their application has at least one confidential OAuth client registered (any client with `type: \"web\" | \"native\" | \"user-agent-based\"` in the `oauthApplication` table, or any `trustedClients` entry without `type: \"public\"`). Public clients with PKCE are not affected.\n- Their application uses `better-auth` at a version below the patched release.\n\nIf an application only uses `@better-auth/oauth-provider` (the canonical replacement for `oidc-provider`) and the `mcp` plugin is not enabled, it is not affected.\n\nFix:\n\n1. Upgrade to `better-auth@1.6.11` or later.\n2. Migrate from the deprecated `oidcProvider()` to `@better-auth/oauth-provider` when feasible. The new package enforces client authentication on both grants by default.\n3. If developers cannot upgrade their applications, see workarounds below.\n\n### Summary\n\nThe legacy `oidcProvider` and `mcp` plugins each expose an OAuth 2.0 token endpoint whose `refresh_token` grant authenticates the request entirely on possession of the bound `refreshToken` row and a matching `client_id`. Neither plugin verifies the registered confidential client's `client_secret` on the refresh path. An attacker who obtains any valid `refresh_token` (via database read, log capture, browser-side XSS, or CORS-amplified script in the mcp case) and the public `client_id` can mint fresh access tokens and rotated refresh tokens until the chain is revoked.\n\n### Details\n\nRFC 6749 §6 and OAuth 2.1 §4.3 require confidential clients to authenticate to the token endpoint on every grant, including refresh. The same plugins' `authorization_code` grant correctly enforces `client_secret` (the oidc-provider via `verifyStoredClientSecret`, the mcp plugin via raw equality), which proves the omission on the refresh path is a regression rather than a design choice.\n\nToken rotation issues a new `refresh_token` with each call, so a single leaked refresh-token grants indefinite access until the row is revoked or its `refreshTokenExpiresAt` (default 7 days) passes; rotation refreshes that window each call.\n\nTwo adjacent issues on the mcp surface ship in the same patch. The mcp `authorization_code` grant uses raw `===` for client-secret comparison and ignores the `storeClientSecret: \"encrypted\" | \"hashed\"` configuration; the fix routes both grants through `verifyStoredClientSecret`. The mcp `/mcp/token` endpoint sets `Access-Control-Allow-Origin: *` unconditionally, which amplifies the refresh bypass in browser contexts; the fix narrows the CORS allowlist.\n\nThe newer `@better-auth/oauth-provider` package routes both grants through `validateClientCredentials` and is not affected.\n\n### Patches\n\nFixed in `better-auth@1.6.11`. The legacy `oidcProvider` and `mcp` token endpoints now require `client_secret` on the `refresh_token` grant for confidential clients, using the same constant-time comparison the `authorization_code` grant already used. Public clients are unaffected (they have no secret to enforce, and PKCE substitutes on the auth-code grant).\n\nThe `Authorization: Basic` parser is fixed to follow RFC 6749 §2.3.1: the credential is split on the first colon and each half is percent-decoded. Client IDs and secrets that contain reserved characters now authenticate correctly. The `/mcp/token` endpoint's CORS configuration is narrowed in the same change (the wildcard `Access-Control-Allow-Origin: *` header is removed), matching the standalone `@better-auth/oauth-provider` package.\n\nThe deprecated `oidc-provider` plugin remains deprecated. The recommended migration path is `@better-auth/oauth-provider`.\n\n### Workarounds\n\nNone of these close the bug fully without a code patch.\n\n- **Migrate to `@better-auth/oauth-provider`** if your deployment can adopt the new plugin. It enforces `client_secret` on both grants.\n- **Force all clients to public + PKCE**: set every client's `type: \"public\"` and require PKCE. The bug is unreachable when there is no `client_secret` to verify.\n- **Network-layer ingress restriction**: limit `/api/auth/oauth2/token` and `/api/auth/mcp/token` to known client IPs at the load balancer. Practical for server-to-server flows, not for end-user-device clients.\n- **Out-of-band refresh-token rotation**: on any suspicion of leak, run `db.deleteMany({ model: \"oauthAccessToken\", where: [{ field: \"clientId\", value: <id> }] })` to invalidate all refresh tokens for the affected client.\n- **For the mcp endpoint specifically**: drop the wildcard CORS at an upstream proxy and replace with a tight allowlist.\n\n### Impact\n\n- **Indefinite confidential-client impersonation**: an attacker holding any valid `refresh_token` and the public `client_id` can mint access tokens and rotated refresh tokens indefinitely, until the row is revoked. Rotation refreshes the expiration window each call.\n- **Resource access at the user's authorized scope**: every minted access token carries the original user's authorization scope, so the attacker reads or writes whatever the resource server grants for that scope.\n\n### Credit\n\nReported by @subhanUmer.\n\n### Resources\n\n- [CWE-306: Missing Authentication for Critical Function](https://cwe.mitre.org/data/definitions/306.html)\n- [CWE-287: Improper Authentication](https://cwe.mitre.org/data/definitions/287.html)\n- [CWE-345: Insufficient Verification of Data Authenticity](https://cwe.mitre.org/data/definitions/345.html)\n- [CWE-863: Incorrect Authorization](https://cwe.mitre.org/data/definitions/863.html)\n- [RFC 6749 §6: Refreshing an Access Token](https://datatracker.ietf.org/doc/html/rfc6749#section-6)\n- [OAuth 2.1 §4.3: Refresh Token](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1#section-4.3)",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N"
  }, {
    "type" : "CVSS_V4",
    "score" : "CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "npm",
      "name" : "better-auth"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "1.6.11"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/better-auth/better-auth/security/advisories/GHSA-pw9m-5jxm-xr6h"
  }, {
    "type" : "ADVISORY",
    "url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-53512"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/better-auth/better-auth/pull/9576"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/better-auth/better-auth/commit/1f2ff4215c4affff0b140b0c0a712c0dde35659c"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/better-auth/better-auth"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/better-auth/better-auth/releases/tag/v1.6.11"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-287", "CWE-306", "CWE-345", "CWE-863" ],
    "severity" : "CRITICAL",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-07-07T20:11:50Z",
    "nvd_published_at" : "2026-07-15T18:16:47Z"
  }
}