{"id":"GHSA-7w99-5wm4-3g79","summary":"@better-auth/oauth-provider's OAuth authorization-code grant allows concurrent redemption when two token requests race the find-then-delete primitive","details":"### Am I affected?\n\nUsers are affected if all of the following are true:\n\n- Their project depends on `@better-auth/oauth-provider` at a version `\u003e= 1.6.0, \u003c 1.6.11`, or uses the embedded plugin in `better-auth \u003e= 1.4.8-beta.7, \u003c 1.6.0`, or enables the legacy `oidc-provider` or `mcp` plugins from `better-auth/plugins`.\n- Their application exposes `/api/auth/oauth2/token` (or the legacy plugins' `/oauth2/token` and `/mcp/token`) as a token endpoint to OAuth/OIDC clients, including internal MCP clients (Claude Desktop, custom MCP tool callers, AI agents).\n- 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.\n\nFix:\n\n1. Upgrade to `@better-auth/oauth-provider@1.6.11` or later. If developers use the legacy plugin paths from `better-auth/plugins`, upgrade `better-auth` to `1.6.11` or later.\n2. If developers cannot upgrade, see workarounds below.\n\n### Summary\n\nThe 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.\n\n### Details\n\nThe 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\u003cvoid\u003e`, 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.\n\nThe 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.\n\n### Patches\n\nFixed in `@better-auth/oauth-provider@1.6.11` and `better-auth@1.6.11` 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`.\n\nError-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.\n\n### Workarounds\n\nNone of these close the bug fully without a code patch. Upgrading is the only good path.\n\n- **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 `code` parameter and serializes concurrent requests for the same code. Fragile under multi-instance deployments unless the registry is shared (Redis-backed).\n- **Database-layer**: add a SQL or Mongo uniqueness constraint that prevents two `oauthAccessToken` rows 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.\n- **Application-layer**: wrap `deleteVerificationByIdentifier` with a custom hook that uses `adapter.deleteMany` and surfaces the count, then injects an `invalid_grant` rejection when the count is zero. Requires forking the internal adapter.\n\n### Impact\n\n- **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.\n- **Detection bypass**: standard OAuth single-use enforcement does not fire for the second redemption when both requests interleave through the read step.\n- **Legacy-plugin reach**: `oidc-provider` and `mcp` plugins share the primitive on the same surface, so deployments using them inherit the same impact.\n\n### Credit\n\nReported by @chdanielmueller.\n\n### Resources\n\n- [CWE-362: Concurrent Execution using Shared Resource with Improper Synchronization (Race Condition)](https://cwe.mitre.org/data/definitions/362.html)\n- [CWE-367: Time-of-check Time-of-use (TOCTOU) Race Condition](https://cwe.mitre.org/data/definitions/367.html)\n- [CWE-294: Authentication Bypass by Capture-replay](https://cwe.mitre.org/data/definitions/294.html)\n- [RFC 6749 §4.1.2: Authorization Response](https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2)\n- [OAuth 2.1 §4.1: Authorization Code Grant](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1#section-4.1)","aliases":["CVE-2026-53518"],"modified":"2026-07-20T19:30:24.125273382Z","published":"2026-07-07T20:56:35Z","database_specific":{"severity":"HIGH","github_reviewed":true,"github_reviewed_at":"2026-07-07T20:56:35Z","nvd_published_at":"2026-07-15T18:16:48Z","cwe_ids":["CWE-294","CWE-362","CWE-367"]},"references":[{"type":"WEB","url":"https://github.com/better-auth/better-auth/security/advisories/GHSA-7w99-5wm4-3g79"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-53518"},{"type":"WEB","url":"https://github.com/better-auth/better-auth/commit/b4bc65a007784b2eb0efb459e5fa6fd8055d3ec9"},{"type":"PACKAGE","url":"https://github.com/better-auth/better-auth"},{"type":"WEB","url":"https://github.com/better-auth/better-auth/releases/tag/v1.6.11"}],"affected":[{"package":{"name":"@better-auth/oauth-provider","ecosystem":"npm","purl":"pkg:npm/%40better-auth/oauth-provider"},"ranges":[{"type":"SEMVER","events":[{"introduced":"1.6.0"},{"fixed":"1.6.11"}]}],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/07/GHSA-7w99-5wm4-3g79/GHSA-7w99-5wm4-3g79.json"}},{"package":{"name":"better-auth","ecosystem":"npm","purl":"pkg:npm/better-auth"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"1.6.11"}]}],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/07/GHSA-7w99-5wm4-3g79/GHSA-7w99-5wm4-3g79.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:N"},{"type":"CVSS_V4","score":"CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:P/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N"}]}