Overview

The OAuth refresh-token rotation vulnerability in Better Auth is a critical issue that affects applications using the `@better-auth/oauth-provider` library at versions `>= 1.6.0, < 1.6.11` or the embedded plugin in `better-auth >= 1.4.8-beta.7, < 1.6.0`. This vulnerability is particularly concerning because it allows an attacker to gain indefinite access to a user's account if they can steal a refresh token.

Understanding the Vulnerability / Threat

Root Cause Analysis

The root cause of this vulnerability lies in the non-atomic read/validate/revoke/mint sequence on the `oauthRefreshToken` row in the `POST /oauth2/token` endpoint during the `refresh_token` grant. Specifically, the `adapter.update` predicate only checks the `id` and not `revoked IS NULL`, allowing two concurrent updates to succeed. This results in a forked refresh-token family from a single parent token. This issue belongs to the CWE-362 category, which involves concurrent execution using shared resources with improper synchronization, also known as a race condition.

Attack Surface & Vector

The attack surface for this vulnerability is significant because it can be exploited under certain conditions: - The project must depend on `@better-auth/oauth-provider` at a version `>= 1.6.0, < 1.6.11` or use the embedded plugin in `better-auth >= 1.4.8-beta.7, < 1.6.0`. - At least one OAuth client must request the `offline_access` scope, which mints refresh tokens. - Concurrent redemption of the same refresh token must be reachable, which can happen in various scenarios such as an SPA sharing a refresh token across browser tabs without a mutex or an attacker timing two requests with a stolen refresh token.

Exploitation Mechanics โ€” Scenario Walkthrough

Scenario: Compromising a Corporate Jenkins Instance 1. Initial Position: An attacker steals a refresh token from a user's session in a corporate environment where the application uses Better Auth for OAuth. 2. Triggering the Flaw: The attacker crafts two concurrent HTTP POST requests to `/oauth2/token` with the stolen refresh token, exploiting the non-atomic check in the refresh-token rotation logic. 3. What Breaks: Both requests pass the revocation check before either revokes the token, resulting in two fresh refresh tokens being minted. The security boundary fails because the check for `revoked` is not atomic with the consumption step. 4. Attacker's Prize: The attacker gains indefinite access to the user's account at the original authorization scope. Even if one token is revoked, the other remains valid, allowing continued access.

Real-World Impact

The impact of this vulnerability is severe. An attacker can achieve: - **Indefinite access** from a single stolen refresh token, as forked refresh-token families grant access at the original user's authorization scope. - **Detection bypass**, as legitimate users with forked refresh tokens do not trip family invalidation when they refresh.

Detection & Defense

Immediate Mitigations

1. **Upgrade to `@better-auth/[email protected]` or later**. 2. If an immediate upgrade is not possible, apply workarounds such as configuring the database adapter to run under serializable isolation or wrapping the `adapter.update` with a row-level pessimistic lock.

Detection Strategies

Defenders can detect exploitation attempts by monitoring for: - Unusual patterns of refresh token usage. - Multiple concurrent requests to the `/oauth2/token` endpoint with the same refresh token.

Long-Term Hardening

- Implement strict refresh-token family invalidation on detected reuse, as prescribed by RFC 9700 ยง4.14. - Enforce atomic compare-and-swap operations in the rotation primitive. - Add a unique constraint on `oauthRefreshToken.token` for defense-in-depth.

Key Takeaways

- Understand the race condition in the OAuth refresh-token rotation mechanism. - Recognize the scenarios where concurrent redemption of refresh tokens can be exploited. - Implement immediate mitigations and long-term hardening strategies to prevent exploitation.

Sources

- [GitHub Security Advisories: GHSA-392p-2q2v-4372](https://github.com/advisories/GHSA-392p-2q2v-4372) - [CVE-2026-53517](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2026-53517)