{"id":"CVE-2026-67335","aliases":["GHSA-wxw3-q3m9-c3jr"],"url":"https://o3.security/vulnerability/CVE-2026-67335","summary":"better-auth before 1.6.2 OAuth State Validation Bypass","details":"### Am I affected?\n\nUsers are affected if all of the following are true:\n\n- The application uses `better-auth` at a version below `1.6.2` (or `@better-auth/sso` paired with such a version).\n- `betterAuth({ account: { storeStateStrategy } })` is set to `\"cookie\"`. The default `\"database\"` is not affected.\n- The application wires at least one OAuth provider through `genericOAuth({ config })` with `pkce: false`, or it supplies a custom `getToken` or `tokenUrl` that does not require the stored `codeVerifier`. Stock social providers with PKCE on are not affected.\n- The provider returns arbitrary `code` values to the configured callback URL.\n\nIf users are on `better-auth@1.6.2` or later, they are not affected.\n\nFix:\n\n1. Upgrade to `better-auth@1.6.2` or later (current stable is `1.6.10`).\n2. If users cannot upgrade, see workarounds below.\n\n### Summary\n\nIn `parseGenericState`, the cookie branch decrypted the `oauth_state` cookie and validated expiry, but did not compare the incoming OAuth `state` query parameter to the nonce that `generateGenericState` issued at sign-in. Any callback to `/api/auth/oauth2/callback/<providerId>` that arrived with a forged `state` and any `code` was therefore accepted as long as the browser still held a live `oauth_state` cookie. With `pkce: false` (or any `getToken` path that does not enforce a code-verifier round-trip), an attacker who forced the victim to deliver an attacker-controlled authorization code to the callback would mint a session bound to the attacker's external identity in the victim's browser. Account-linking flows behaved the same way, binding the attacker's external account to an authenticated victim row.\n\n### Details\n\nThe cookie branch of `parseGenericState` did not compare the cookie's stored nonce to the incoming `state` parameter. The database branch (the default) was not affected because the verification row is keyed by `state` and the lookup itself enforces equality.\n\nThe fix re-binds the cookie to the nonce: `generateGenericState` writes `oauthState: state` into the encrypted payload before storage, and `parseGenericState` rejects when `parsedData.oauthState !== state`. The same primitive covers every caller (`generic-oauth`, social, account-link, oauth-proxy passthrough, OIDC SSO, SAML relay state).\n\n### Patches\n\nFixed in `better-auth@1.6.2` via [PR #8949](https://github.com/better-auth/better-auth/pull/8949) (commit `9deb7936a`, merged 2026-04-09). The cookie branch of `parseGenericState` now rejects when the encrypted payload's nonce does not match the incoming `state` parameter; the database branch gained a defense-in-depth equality check.\n\n### Workarounds\n\nIf users cannot upgrade immediately:\n\n- **Switch `storeStateStrategy` back to `\"database\"`** (the default). This closes the cookie-only bypass without a code change.\n- **Enable `pkce: true`** on every affected `genericOAuth` provider. The `codeVerifier` is the missing primitive that the attacker cannot supply.\n\n### Impact\n\n- **Forced-login (CSRF on OAuth callback)**: the attacker forces the victim's browser into an authenticated session bound to the attacker's external identity, allowing the attacker to observe the victim's actions inside the application.\n- **Persistent account linking**: account-link flows bind the attacker's external account to the victim's authenticated row, granting persistent access until the link is removed.\n\n### Credit\n\nReported by @Jvr2022 via private advisory disclosure, and by @alavesa (PatchPilots audit) via the public duplicate [issue #8897](https://github.com/better-auth/better-auth/issues/8897).\n\n### Resources\n\n- [CWE-352: Cross-Site Request Forgery (CSRF)](https://cwe.mitre.org/data/definitions/352.html)\n- [CWE-345: Insufficient Verification of Data Authenticity](https://cwe.mitre.org/data/definitions/345.html)\n- [CWE-287: Improper Authentication](https://cwe.mitre.org/data/definitions/287.html)\n- [RFC 6749 §10.12: Cross-Site Request Forgery](https://datatracker.ietf.org/doc/html/rfc6749#section-10.12)\n- [RFC 7636: Proof Key for Code Exchange](https://datatracker.ietf.org/doc/html/rfc7636)","published":"2026-08-01T12:22:17.178Z","modified":"2026-09-10T03:30:25.855015435Z","cvss":null,"epss":{"score":0.00172,"percentile":0.0691,"asOf":"2026-08-16"},"cisaKev":null,"exploitsKnown":0,"affectedPackages":[{"ecosystem":"npm","name":"better-auth","fixedVersion":"1.6.2"}],"fix":{"url":"https://github.com/better-auth/better-auth/pull/8949","label":"better-auth/better-auth#8949"},"references":[{"type":"ADVISORY","url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/67xxx/CVE-2026-67335.json"},{"type":"ADVISORY","url":"https://github.com/better-auth/better-auth/security/advisories/GHSA-wxw3-q3m9-c3jr"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-67335"},{"type":"ADVISORY","url":"https://www.vulncheck.com/advisories/better-auth-before-oauth-state-validation-bypass"},{"type":"WEB","url":"https://github.com/better-auth/better-auth/issues/8897"},{"type":"WEB","url":"https://github.com/better-auth/better-auth/pull/8949"},{"type":"WEB","url":"https://github.com/better-auth/better-auth/commit/9deb7936aba7931f2db4b460141f476508f11bfd"},{"type":"PACKAGE","url":"https://github.com/better-auth/better-auth"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-09-10T03:30:25.855015435Z"}}