{"id":"CVE-2026-53512","aliases":["GHSA-pw9m-5jxm-xr6h"],"title":"Better Auth: OAuth refresh-token replay via missing client authentication on oidc-provider and mcp plugins","summary":"Better Auth: OAuth refresh-token replay via missing client authentication on oidc-provider and mcp plugins","severity":"critical","cvss":9.1,"cwe":["CWE-287","CWE-306","CWE-345","CWE-863"],"vendor":"better-auth","product":"better-auth","ecosystem":"npm","affected":["better-auth < 1.6.11"],"patched":["better-auth 1.6.11"],"published":"2026-07-07","updated":"2026-07-07","source":"GHSA","sourceUrl":"https://github.com/advisories/GHSA-pw9m-5jxm-xr6h","references":[{"url":"https://github.com/better-auth/better-auth/security/advisories/GHSA-pw9m-5jxm-xr6h"},{"url":"https://github.com/better-auth/better-auth/releases/tag/v1.6.11"},{"url":"https://github.com/advisories/GHSA-pw9m-5jxm-xr6h"}],"tags":["ghsa","npm"],"ingestedAt":"2026-07-07T20:43:04.975Z","epss":0.00275,"epssPercentile":0.20132,"slug":"CVE-2026-53512","body":"## Overview\n\n### 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)\n\n## Affected packages\n\n- `better-auth < 1.6.11`\n\n## Remediation\n\nUpgrade to a patched release:\n\n- `better-auth 1.6.11`","depth":"midnight","depthScore":50,"depthScoreParts":{"impact":50.1,"likelihood":0.1,"exploitation":0,"ransomware":0},"changes":[]}