CVE-2026-53512Critical· 9.1▾ MidnightBetter Auth: OAuth refresh-token replay via missing client authentication on oidc-provider and mcp plugins
▾ Midnight zone — Critical, or high with PoC / in-the-wild
impact 50.1 · likelihood 0.1 · exploitation 0
Need a working PoC? Pro members can cast a request and our team develops one — it lands right here.
Exploit-prediction probability, daily snapshots since Jul 16.
Disclosure to exploitation, from the record and what we observed since indexing it.
Disclosed via GHSA
0.3%
Users are affected if all of the following are true:
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).type: "web" | "native" | "user-agent-based" in the oauthApplication table, or any trustedClients entry without type: "public"). Public clients with PKCE are not affected.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:
[email protected] or later.oidcProvider() to @better-auth/oauth-provider when feasible. The new package enforces client authentication on both grants by default.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.
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.
Fixed in [email protected]. 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.
None of these close the bug fully without a code patch.
@better-auth/oauth-provider if your deployment can adopt the new plugin. It enforces client_secret on both grants.type: "public" and require PKCE. The bug is unreachable when there is no client_secret to verify./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.db.deleteMany({ model: "oauthAccessToken", where: [{ field: "clientId", value: <id> }] }) to invalidate all refresh tokens for the affected client.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.Reported by @subhanUmer.
better-auth < 1.6.11Upgrade to a patched release:
better-auth 1.6.11Connected by shared product, vendor, weakness, or advisory.
CVE-2026-53514High· 7.7Better Auth vulnerable to unauthorized invitation acceptance via unverified email match in organization plugin
CVE-2026-53516High· 8.3Better Auth has an account takeover issue via OAuth auto-link to unverified pre-registered email
GHSA-qq9h-g4jm-xgf3High· 8.3Better Auth: Account takeover via pre-account hijacking on magic-link and email-OTP sign-in
CVE-2026-53515High· 7.1@better-auth/sso: SSO provider may allow registration for any org member without a checking their role
GHSA-9h47-pqcx-hjr4High· 8.7Better Auth has insecure cryptographic defaults in oidcProvider: alg=none advertised and plain PKCE accepted by default
CVE-2026-53513Critical· 9.6@better-auth/sso provider registration has server-side request forgery via unvalidated OIDC endpoints