{"id":"CVE-2026-53512","aliases":["EUVD-2026-44735","GHSA-pw9m-5jxm-xr6h"],"status":"published","title":"Better Auth: OAuth refresh-token replay via missing client authentication on oidc-provider and mcp plugins","description":"Better Auth is an authentication and authorization library for TypeScript. Prior to 1.6.11, the legacy oidcProvider and mcp plugins expose OAuth token endpoints whose refresh_token grant authenticates only possession of the bound refreshToken row and matching client_id, without verifying the confidential client's client_secret, allowing an attacker with a valid refresh_token to mint access tokens and rotated refresh tokens through /api/auth/oauth2/token or /api/auth/mcp/token. The @better-auth/oauth-provider package is not affected. This issue is fixed in version 1.6.11.","severity":{"version":"4.0","score":9.1,"vector":"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","level":"critical","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-11","score":0.00275,"percentile":0.20152,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":false,"fix_available":true},"cwes":["CWE-287","CWE-306","CWE-345","CWE-863"],"published":"2026-07-07T20:11:50Z","modified":"2026-10-28T02:30:26Z","fixes":[{"ecosystem":"npm","package":"better-auth","fixed_in":["1.6.11"],"sources":["ghsa","osv"]}],"affected":[{"source":"ghsa","ecosystem":"npm","package":"better-auth","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"1.6.11"}]}]},{"source":"osv","ecosystem":"npm","package":"better-auth","ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"1.6.11"}]}]}],"products":[{"vendor":"better-auth","product":"better-auth"},{"vendor":"better-auth","product":"better_auth"}],"references":[{"url":"https://github.com/better-auth/better-auth/security/advisories/GHSA-pw9m-5jxm-xr6h","tags":["x_refsource_CONFIRM","Mitigation","Vendor Advisory","advisory","web"]},{"url":"https://github.com/better-auth/better-auth/pull/9576","tags":["x_refsource_MISC","Issue Tracking","Patch","fix","web"]},{"url":"https://github.com/better-auth/better-auth/commit/1f2ff4215c4affff0b140b0c0a712c0dde35659c","tags":["x_refsource_MISC","Patch","fix","web"]},{"url":"https://github.com/better-auth/better-auth/releases/tag/v1.6.11","tags":["x_refsource_MISC","Release Notes","web"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-53512","tags":["advisory"]},{"url":"https://github.com/advisories/GHSA-pw9m-5jxm-xr6h"},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/53xxx/CVE-2026-53512.json","tags":["advisory"]},{"url":"https://github.com/better-auth/better-auth","tags":["package"]}],"sources":{"cve":[{"id":"CVE-2026-53512","title":"Better Auth: OAuth refresh-token replay via missing client authentication on oidc-provider and mcp plugins","description":"Better Auth is an authentication and authorization library for TypeScript. Prior to 1.6.11, the legacy oidcProvider and mcp plugins expose OAuth token endpoints whose refresh_token grant authenticates only possession of the bound refreshToken row and matching client_id, without verifying the confidential client's client_secret, allowing an attacker with a valid refresh_token to mint access tokens and rotated refresh tokens through /api/auth/oauth2/token or /api/auth/mcp/token. The @better-auth/oauth-provider package is not affected. This issue is fixed in version 1.6.11.","status":"published","published":"2026-07-15T17:18:09.512Z","modified":"2026-07-18T01:08:08.419Z","cvss":[{"version":"4.0","score":9.1,"vector":"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","level":"critical","assessor":"cna"}],"cwes":["CWE-287","CWE-306","CWE-345","CWE-863"],"updated_at":"2026-11-02T14:20:00.11477Z"}],"euvd":[{"id":"EUVD-2026-44735","description":"Better Auth is an authentication and authorization library for TypeScript. Prior to 1.6.11, the legacy oidcProvider and mcp plugins expose OAuth token endpoints whose refresh_token grant authenticates only possession of the bound refreshToken row and matching client_id, without verifying the confidential client's client_secret, allowing an attacker with a valid refresh_token to mint access tokens and rotated refresh tokens through /api/auth/oauth2/token or /api/auth/mcp/token. The @better-auth/oauth-provider package is not affected. This issue is fixed in version 1.6.11.","status":"published","published":"2026-07-15T17:18:09Z","modified":"2026-07-18T01:08:08Z","cvss":[{"version":"4.0","score":9.1,"vector":"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","level":"critical"}],"updated_at":"2026-11-02T18:19:50.065723Z"}],"ghsa":[{"id":"GHSA-pw9m-5jxm-xr6h","title":"Better Auth: OAuth refresh-token replay via missing client authentication on oidc-provider and mcp plugins","description":"### 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: \u003cid\u003e }] })` 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)","status":"published","published":"2026-07-07T20:11:50Z","modified":"2026-07-20T19:24:32Z","cvss":[{"version":"4.0","score":9.1,"vector":"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","level":"critical"},{"version":"3.1","score":9.1,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N","level":"critical"}],"cwes":["CWE-287","CWE-306","CWE-345","CWE-863"],"updated_at":"2026-11-02T14:40:47.245721Z"}],"nvd":[{"id":"CVE-2026-53512","description":"Better Auth is an authentication and authorization library for TypeScript. Prior to 1.6.11, the legacy oidcProvider and mcp plugins expose OAuth token endpoints whose refresh_token grant authenticates only possession of the bound refreshToken row and matching client_id, without verifying the confidential client's client_secret, allowing an attacker with a valid refresh_token to mint access tokens and rotated refresh tokens through /api/auth/oauth2/token or /api/auth/mcp/token. The @better-auth/oauth-provider package is not affected. This issue is fixed in version 1.6.11.","status":"published","published":"2026-07-15T18:16:47.42Z","modified":"2026-07-21T13:33:47.543Z","cvss":[{"version":"4.0","score":9.1,"vector":"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","level":"critical","assessor":"secondary"},{"version":"3.1","score":9.1,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N","level":"critical","assessor":"primary"}],"cwes":["CWE-287","CWE-306","CWE-345","CWE-863"],"updated_at":"2026-11-02T14:12:29.879716Z"}],"osv":[{"id":"CVE-2026-53512","title":"Better Auth: OAuth refresh-token replay via missing client authentication on oidc-provider and mcp plugins","description":"Better Auth is an authentication and authorization library for TypeScript. Prior to 1.6.11, the legacy oidcProvider and mcp plugins expose OAuth token endpoints whose refresh_token grant authenticates only possession of the bound refreshToken row and matching client_id, without verifying the confidential client's client_secret, allowing an attacker with a valid refresh_token to mint access tokens and rotated refresh tokens through /api/auth/oauth2/token or /api/auth/mcp/token. The @better-auth/oauth-provider package is not affected. This issue is fixed in version 1.6.11.","status":"published","published":"2026-07-15T17:18:09.512Z","modified":"2026-10-28T02:30:26.600000593Z","cvss":[{"version":"4.0","score":9.1,"vector":"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","level":"critical"}],"cwes":["CWE-287","CWE-306","CWE-345","CWE-863"],"updated_at":"2026-11-02T15:45:08.132336Z"},{"id":"GHSA-pw9m-5jxm-xr6h","title":"Better Auth: OAuth refresh-token replay via missing client authentication on oidc-provider and mcp plugins","description":"### 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: \u003cid\u003e }] })` 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)","status":"published","published":"2026-07-07T20:11:50Z","modified":"2026-07-20T19:30:24.092604956Z","cvss":[{"version":"3.1","score":9.1,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N","level":"critical"},{"version":"4.0","score":9.1,"vector":"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","level":"critical"}],"cwes":["CWE-287","CWE-306","CWE-345","CWE-863"],"updated_at":"2026-11-02T15:46:53.700556Z"}]}}
