CVE Patch Review

GHSA-XMF8-CVQR-RFGJ: Root-cause Fix for Auth.js Bearer Token DoS and OAuth Provider Cookie Replay

GHSA-XMF8-CVQR-RFGJ · Updated 2026-07-24 Root-cause

Summary

The patch addresses two distinct flaws in Auth.js: an uncaught exception path in JWT Bearer token parsing that allowed unauthenticated denial of service via malformed percent-encoding, and missing provider binding in OAuth state/nonce/PKCE cookies that enabled cross-provider replay/session confusion. The changes are narrowly targeted, add regression coverage, and convert both failure modes into explicit rejection paths.

Analysis

Vulnerability

GHSA-XMF8-CVQR-RFGJ covers two security issues in Auth.js.

First, JWT extraction from the Authorization: Bearer ... header called decodeURIComponent on attacker-controlled input without handling decoding failures. Inputs such as %, %A, or %ZZ trigger a URIError. Because this exception was not caught, an unauthenticated request could force an application error path and potentially crash request handling, producing a denial of service. This behavior is documented in the JWT patch series for both the core and next-auth packages in PR #13467, PR #13469, and commits e707770 and 5bca239.

Second, OAuth check cookies for state, nonce, and pkce stored only a value and were not bound to the provider that minted them. That meant a cookie created during one provider flow could be replayed during another provider callback if other conditions aligned, creating cross-provider session confusion. The fix adds a provider identifier into the sealed token payload and rejects cookies whose embedded provider does not match the callback handler's provider. This logic appears in PR #13469 and commit 5bca239.

// Vulnerable Bearer token handling
token = decodeURIComponent(urlEncodedToken)

// Patched behavior
try {
  token = decodeURIComponent(urlEncodedToken)
} catch {
  return null
}

// Vulnerable OAuth check payload
token: { value },

// Patched OAuth check payload and validation
token: { value, provider: options.provider.id },
if (value.provider !== options.provider?.id)
  throw new TypeError(
    `${name} cookie was created for a different provider than the one handling the callback.`
  )

Patch

The patch is split across two code paths.

For JWT parsing, the implementation now treats malformed percent-encoding as an invalid Bearer token rather than an exceptional condition. In packages/next-auth/src/jwt/index.ts and packages/core/src/jwt.ts, decodeURIComponent(urlEncodedToken) is wrapped in a try/catch and returns null on failure. The associated tests verify that malformed values resolve to null and that valid encoded JWTs still work. See e707770, 5bca239, PR #13467, and PR #13469.

For OAuth checks, the patch changes the cookie token schema from a bare { value } payload to { value, provider: options.provider.id }. On consumption, each check path validates that the embedded provider matches options.provider.id and throws if it does not. Regression tests cover three cases for each of state, nonce, and pkce: same-provider acceptance, different-provider rejection, and rejection of legacy cookies that lack the new provider field. See PR #13469 and 5bca239.

Review

Pros

  • The DoS fix addresses the actual fault boundary: untrusted header decoding no longer propagates a runtime exception. Returning null is consistent with existing token-not-found semantics and is a safe failure mode.
  • The OAuth fix binds security-critical transient cookies to provider identity, which directly closes the replay/confusion class rather than relying on incidental flow separation.
  • Test coverage is strong and source-aligned. The JWT tests exercise malformed percent-encoding and valid encoded tokens, while the OAuth tests cover positive and negative provider-binding cases across all three check types.
  • The patch is low-complexity and localized, reducing regression risk in unrelated authentication logic.
  • Rejecting legacy cookies without a provider field is conservative and security-favorable; it avoids silently accepting ambiguous state created under the old scheme.

Cons

  • The OAuth cookie schema change is intentionally breaking for in-flight or preexisting transient cookies. Users may see callback failures during rollout if an authorization flow started before upgrade and completed after upgrade.
  • The next-auth JWT patch uses a // @ts-expect-error comment on the return null path in one snippet, which suggests a type mismatch remains in the API contract rather than being fully modeled in types.
  • The provider-binding check is repeated across multiple check handlers, which is acceptable but duplicates logic. A shared helper could reduce future drift if additional check types are added.
  • The available patch snippets do not show any explicit logging or telemetry on malformed Bearer tokens or provider mismatches, so operators may have limited visibility into active exploitation attempts unless higher layers log these failures.

Verdict

Root-cause.

This patch fixes both vulnerabilities at the correct trust boundaries. The Bearer token issue is resolved by converting malformed percent-decoding from an uncaught exception into a normal invalid-token result, eliminating the unauthenticated crash vector described in the advisory. The OAuth issue is resolved by adding missing context binding to transient cookies and enforcing that binding on callback, which directly prevents cross-provider replay/session confusion rather than merely filtering a subset of bad inputs.

From a security engineering perspective, this is a complete and technically sound remediation. The main operational caveat is compatibility with legacy or in-flight OAuth cookies, but that is an expected consequence of correcting an under-specified token format. Overall, the patch should be accepted and deployed promptly.

Sources