Severity by source
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
AV:N and PR:N because the token endpoint needs no client credentials, but AC:H because exploitation depends on separately obtaining a leaked refresh_token, a precondition outside attacker control; C/I high, A:N.
Primary rating from Vendor (https://github.com/better-auth/better-auth).
CVSS VectorVendor: https://github.com/better-auth/better-auth
Lifecycle Timeline
1Blast Radius
ecosystem impact- 26 npm packages depend on better-auth (19 direct, 7 indirect)
Ecosystem-wide dependent count for version 1.6.11.
DescriptionCVE.org
Am I affected?
Users are affected if all of the following are true:
- Their application uses
better-authand has enabled at least one of:oidcProvider()(imported frombetter-auth/plugins/oidc-provider), ormcp()(imported frombetter-auth/plugins/mcp). - Their application has at least one confidential OAuth client registered (any client with
type: "web" | "native" | "user-agent-based"in theoauthApplicationtable, or anytrustedClientsentry withouttype: "public"). Public clients with PKCE are not affected. - Their application uses
better-authat 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:
- Upgrade to
better-auth@1.6.11or later. - Migrate from the deprecated
oidcProvider()to@better-auth/oauth-providerwhen feasible. The new package enforces client authentication on both grants by default. - 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-providerif your deployment can adopt the new plugin. It enforcesclient_secreton both grants. - Force all clients to public + PKCE: set every client's
type: "public"and require PKCE. The bug is unreachable when there is noclient_secretto verify. - Network-layer ingress restriction: limit
/api/auth/oauth2/tokenand/api/auth/mcp/tokento 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_tokenand the publicclient_idcan 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
AnalysisAI
Confidential-client impersonation in better-auth below 1.6.11 lets an attacker who holds any leaked refresh_token and the public client_id mint fresh access tokens and rotated refresh tokens indefinitely. The deprecated oidcProvider() and mcp() plugins expose an OAuth 2.0 token endpoint whose refresh_token grant authenticates solely on the bound refresh-token row plus a matching client_id, never verifying the registered client_secret - a regression, since the same plugins' authorization_code grant does enforce the secret. No public exploit identified at time of analysis; the flaw was reported by @subhanUmer and fixed by the maintainers, with no CISA KEV listing or EPSS score supplied in the input.
Technical ContextAI
better-auth is a TypeScript/JavaScript authentication framework distributed on npm (pkg:npm/better-auth). The affected surface is its OAuth 2.0 / OIDC authorization-server functionality provided by two legacy plugins: oidcProvider() (better-auth/plugins/oidc-provider) and mcp() (better-auth/plugins/mcp), each of which stands up a token endpoint (/api/auth/oauth2/token and /api/auth/mcp/token respectively). The root cause maps to CWE-287 (Improper Authentication), and more precisely CWE-306 (Missing Authentication for Critical Function): RFC 6749 §6 and OAuth 2.1 §4.3 require confidential clients to authenticate to the token endpoint on every grant including refresh, but these plugins omit the client_secret check on the refresh path while enforcing it (via verifyStoredClientSecret in oidc-provider and raw equality in mcp) on the authorization_code path. Token rotation issues a new refresh_token per call and resets refreshTokenExpiresAt (default 7 days), so a single leak yields an indefinitely renewable session. Two adjacent mcp defects ship in the same patch: the mcp authorization_code grant used raw === comparison and ignored storeClientSecret ("encrypted"|"hashed"), and the /mcp/token endpoint returned Access-Control-Allow-Origin: * unconditionally, which lets browser-side script (XSS/CORS) reach the endpoint and amplify the bypass. The newer @better-auth/oauth-provider package routes both grants through validateClientCredentials and is not affected.
RemediationAI
Vendor-released patch: better-auth@1.6.11 - upgrade to 1.6.11 or later, which requires client_secret on the refresh_token grant for confidential clients using the same constant-time comparison the authorization_code grant already used, routes the mcp authorization_code grant through verifyStoredClientSecret (honoring storeClientSecret), fixes the RFC 6749 §2.3.1 Authorization: Basic parser, and removes the wildcard CORS header from /mcp/token (per https://github.com/better-auth/better-auth/releases/tag/v1.6.11 and advisory GHSA-pw9m-5jxm-xr6h). Where feasible, migrate from the deprecated oidcProvider() to @better-auth/oauth-provider, which enforces client authentication on both grants by default. If you cannot upgrade, effective workarounds each carry trade-offs: force every client to type "public" plus mandatory PKCE (removes the client_secret the bug fails to verify, but drops confidential-client semantics); restrict /api/auth/oauth2/token and /api/auth/mcp/token at the load balancer to known client IPs (works for server-to-server flows, not end-user-device clients); for mcp specifically, strip the wildcard Access-Control-Allow-Origin at an upstream proxy and replace it with a tight allowlist; and on any suspicion of leak, invalidate all refresh tokens for the affected client via db.deleteMany({ model: "oauthAccessToken", where: [{ field: "clientId", value: <id> }] }). None of the workarounds fully close the flaw without the code patch.
The x86-64 kernel system-call functionality in Xen 4.1.2 and earlier, as used in Citrix XenServer 6.0.2 and earlier and
An authenticated path traversal vulnerability in Langflow's file upload functionality allows attackers to write arbitrar
Arbitrary host file write in Incus before 7.1.0 lets a holder of S3 bucket credentials escape the storage volume via a p
Canonical snapd before version 2.37.1 incorrectly performed socket owner validation, allowing an attacker to run arbitra
An authorization bypass vulnerability in gRPC-Go allows attackers to circumvent path-based access control by sending HTT
Server-side request forgery in the Ruby css_parser gem (< 3.0.0) lets a remote unauthenticated attacker force the parsin
Arbitrary file read in Langroid's SQLChatAgent (<= 0.63.0) lets an attacker who can influence the LLM-generated SQL exfi
Authentication bypass in the joserfc Python JOSE/JWT library (PyPI, versions <= 1.6.7) lets unauthenticated attackers fo
SSRF in the Bifrost AI gateway's multimodal URL-fetch path permits any client that can submit a Bedrock or Vertex provid
URLSecurityValidator SSRF bypass in compliance-trestle <= 4.0.3 allows a network-positioned attacker to reach cloud meta
Path traversal in rclone's `serve restic` REST API allows any attacker with network access to the endpoint to read, crea
Credential leakage in elixir-tesla (Tesla HTTP client for Elixir) versions 1.4.0 through 1.18.2 allows Authorization and
Same weakness CWE-287 – Improper Authentication
View allSame technique Authentication Bypass
View allShare
External POC / Exploit Code
Leaving vuln.today
EUVD-2026-44735
GHSA-pw9m-5jxm-xr6h