CVE-2026-53518: @better-auth/oauth-provider's OAuth authorization-code grant allows concurrent redemption when two token requests race the find-then-delete primitive
Better Auth's OAuth provider redeems a single-use authorization code with a non-atomic find-then-delete. Two token requests sent at the same moment with the same code both succeed, and each gets its own access, refresh and id tokens. Affects @better-auth/oauth-provider 1.6.0 to 1.6.10, the embedded plugin and the legacy oidc-provider and mcp plugins. Fixed in 1.6.11.
Published Jul 7, 2026 · Updated Jul 20, 2026 · Analysis Oct 25, 2026
What an attacker can do
On the authorization_code grant, POST /oauth2/token reads the verification row for the code, then deletes it. Two concurrent requests both pass the read before either delete lands. Both then check PKCE and call createUserTokens. RFC 6749 section 4.1.2 requires codes to be single use, and this race breaks that rule.
The result is several independent token sets from one user authorization, all valid for that user's scope. The single-use check that would normally flag a replayed code does not fire when the requests interleave. An attacker needs a captured code. The CVSS vector marks user interaction (the user must authorize) and no privileges. The practical case is a stolen or intercepted code that the legitimate client redeems at the same time as the attacker, leaving the user with no sign that a second token set exists.
The legacy oidc-provider and mcp plugins share the same primitive. That matters for apps that expose a token endpoint to MCP clients such as Claude Desktop, custom MCP callers or AI agents. The maintainers note 20 other call sites use the same find-then-delete pattern.
Who is affected
Affected: @better-auth/oauth-provider from 1.6.0 up to but not including 1.6.11, and the embedded plugin in better-auth from 1.4.8-beta.7 before 1.6.0. The legacy oidc-provider and mcp plugins from better-auth/plugins are affected on better-auth below 1.6.11. You are exposed if you serve /api/auth/oauth2/token, /oauth2/token or /mcp/token and have no external mitigation such as an idempotency cache keyed by code.
npm ls @better-auth/oauth-provider better-auth@better-auth/oauth-provider
- Ecosystem
- npm
- Affected
- >= 1.6.0, < 1.6.11
- Fixed in
- 1.6.11
- Check
npm ls @better-auth/oauth-provider
better-auth
- Ecosystem
- npm
- Affected
- < 1.6.11
- Fixed in
- 1.6.11
- Check
npm ls better-auth
How to fix it
Upgrade @better-auth/oauth-provider to 1.6.11 or later. If you use the legacy plugin paths, upgrade better-auth to 1.6.11 or later.
npm install @better-auth/[email protected] [email protected]The patch routes all three token-exchange call sites through internalAdapter.consumeVerificationValue, which deletes the row and returns its value in one operation. The first request wins. Racing requests get invalid_grant. The release also adds a unique constraint on oauthRefreshToken.token in a related fix, so check whether you need to run your database migration.
The advisory says no workaround fully closes the bug. Partial options are a reverse proxy that serializes requests per code, a database uniqueness rule, or a wrapper that rejects when deleteMany removes zero rows.
How to tell if you were hit
Look in token-endpoint logs and in the token table for two issuances tied to the same authorization code, with timestamps within milliseconds of each other. Two refresh tokens for one grant that you cannot explain is a sign. The patched endpoint returns invalid_grant for the losing request, so those errors in logs after the upgrade show attempted races. If you cannot upgrade, revoke tokens issued from suspect grants and ask the user to re-authorize.
Original advisory
From the GitHub Advisory Database (CC BY 4.0).
Am I affected?
Users are affected if all of the following are true:
- Their project depends on
@better-auth/oauth-providerat a version>= 1.6.0, < 1.6.11, or uses the embedded plugin inbetter-auth >= 1.4.8-beta.7, < 1.6.0, or enables the legacyoidc-providerormcpplugins frombetter-auth/plugins. - Their application exposes
/api/auth/oauth2/token(or the legacy plugins'/oauth2/tokenand/mcp/token) as a token endpoint to OAuth/OIDC clients, including internal MCP clients (Claude Desktop, custom MCP tool callers, AI agents). - Their application has not implemented an external mitigation: a load-balancer-level idempotency cache keyed by
code, a database trigger that rejects duplicate token issuance for the same authorization code, or a custom adapter override that performs an atomic compare-and-delete.
Fix:
- Upgrade to
@better-auth/[email protected]or later. If developers use the legacy plugin paths frombetter-auth/plugins, upgradebetter-authto1.6.11or later. - If developers cannot upgrade, see workarounds below.
Summary
The OAuth provider's POST /oauth2/token endpoint, on the authorization_code grant, redeems a single-use authorization code through a non-atomic find-then-delete sequence. Two concurrent requests with the same code value both pass the read step before either delete completes, then both proceed to PKCE verification and createUserTokens. Each surviving request mints a fresh access token, refresh token, and id token. RFC 6749 §4.1.2 requires authorization codes to be single-use; this primitive does not enforce that under concurrency.
Details
The same architectural primitive (find a single-use verification row, then delete it, then trust the row to authorize) is used in 20 other call sites across the codebase. The deletion primitive returns Promise<void>, discarding the row count surfaced by adapter.deleteMany, so no call site can detect "another caller already claimed this row". The fix lands at the primitive layer rather than at any individual call site.
The fix introduces a claimVerificationByIdentifier primitive at the internal-adapter layer that performs an atomic claim-and-return, replaces the find-then-delete pair at this call site, and migrates the highest-impact variant sites in the same release.
Patches
Fixed in @better-auth/[email protected] and [email protected] for the legacy oidc-provider and mcp plugin paths. All three token-exchange call sites now consume the verification row through internalAdapter.consumeVerificationValue, an atomic claim primitive that deletes the row and returns its prior value in one operation. The first request to arrive takes the row and mints tokens; concurrent racers observe an empty result and return invalid_grant.
Error-code consistency is also tightened on the @better-auth/oauth-provider token endpoint: the malformed-verification-value branches previously returned a project-specific invalid_verification code, which is not part of RFC 6749 §5.2's response error set. Both branches now return invalid_grant so spec-compliant clients can branch on the standard code without a special case.
Workarounds
None of these close the bug fully without a code patch. Upgrading is the only good path.
- Network-layer: deploy an authorization-server-aware reverse proxy (Envoy, NGINX with Lua, custom Cloudflare Worker) that holds an in-flight registry keyed by the
codeparameter and serializes concurrent requests for the same code. Fragile under multi-instance deployments unless the registry is shared (Redis-backed). - Database-layer: add a SQL or Mongo uniqueness constraint that prevents two
oauthAccessTokenrows from being created with the same upstream code reference. Adapter-specific and not always feasible since the schema does not currently store the source code. - Application-layer: wrap
deleteVerificationByIdentifierwith a custom hook that usesadapter.deleteManyand surfaces the count, then injects aninvalid_grantrejection when the count is zero. Requires forking the internal adapter.
Impact
- Multiple independent token sets from a single authorization: forked access tokens, refresh tokens, and id tokens issued from the same code, all valid for the original user's authorization scope.
- Detection bypass: standard OAuth single-use enforcement does not fire for the second redemption when both requests interleave through the read step.
- Legacy-plugin reach:
oidc-providerandmcpplugins share the primitive on the same surface, so deployments using them inherit the same impact.
Credit
Reported by @chdanielmueller.
Resources
Credited to chdanielmueller.
Questions
- Does CVE-2026-53518 affect MCP servers?
Yes, if they use Better Auth's
mcpplugin or the OAuth provider. The advisory names the/mcp/tokenendpoint and lists internal MCP clients and AI agents among the callers that can be exposed.- Is there a workaround if I can't upgrade?
Only partial ones. The advisory lists a proxy that serializes requests by
code, a database uniqueness constraint, or an adapter wrapper that rejects when no row was deleted. It says upgrading is the only good path.- What version fixes it?
@better-auth/oauth-provider1.6.11, andbetter-auth1.6.11 for the legacy oidc-provider and mcp plugins.
References
How this page is made: the data panel comes from the GitHub Advisory Database, CISA’s KEV catalog, the NVD and FIRST EPSS, and updates twice a day. The analysis is written by an AI model from the sources listed above and checked by automated rules before it is published. Check versions against the vendor's advisory before you act. Methodology
More Other MCP servers and tools vulnerabilities
All Other MCP servers and tools entriesCVE-2026-77361
obsidian-tc : Symlink read-ACL scope bypass via lexical path checks
medium4.7Oct 23, 2026CVE-2026-114126
Storybook dev server vulnerable to code injection from the local network
high8.7Oct 22, 2026CVE-2026-113731
djust: Arbitrary absolute-path file read in the find_handlers_for_template MCP tool
low3.3Oct 21, 2026CVE-2026-114125
Storybook: Missing origin validation in Storybook preview allows code injection from malicious websites
critical9.4Oct 21, 2026CVE-2026-111770
MCP TypeScript server SDK: Shared transport or server instances delivered one HTTP client's messages to another
high7.7Oct 20, 2026GHSA-953h-535j-jg2v
agent-router: MCP session resume does not verify the embedded anti-hijacking subject; default Helm seed makes session IDs forgeable
medium6.8Oct 19, 2026
