{"id":"GHSA-2vg6-77g8-24mp","summary":"Better Auth: Stale sessions persist after user deletion across admin, anonymous, and SCIM flows","details":"### Am I affected?\n\nUsers are affected if all of the following are true:\n\n- They configure `secondaryStorage` on `betterAuth(...)` (Redis, KV, or any external session cache).\n- `session.storeSessionInDatabase` is left unset or set to `false` (the default).\n- Their application's deployment uses one or more of:\n  - The `admin` plugin and calls `auth.api.removeUser(...)` or `authClient.admin.removeUser(...)`.\n  - The `anonymous` plugin and exposes `/delete-anonymous-user` or relies on the after-link hook to clean up the anonymous user.\n  - The `@better-auth/scim` plugin and exposes `DELETE /scim/v2/Users/:userId`.\n\nIf `storeSessionInDatabase` is `true`, sessions are also written to the database, and the database delete cascades; users are not affected.\n\nFix:\n\n1. Upgrade to `better-auth@\u003cpatched-version\u003e` or later (and `@better-auth/scim@\u003cpatched-version\u003e` if they use SCIM).\n2. If they cannot upgrade, see workarounds below.\n\n### Summary\n\nWhen `secondaryStorage` is configured and `storeSessionInDatabase` is `false`, three user-deletion endpoints in `better-auth` plus one in `@better-auth/scim` call `internalAdapter.deleteUser(userId)` without first calling `internalAdapter.deleteSessions(userId)`. The deleted user's session payload (which carries a cached user object) remains in secondary storage, and `internalAdapter.findSession(token)` keeps returning it as a valid session until the session TTL elapses (default 7 days).\n\n### Details\n\nThe vulnerable call sites are:\n\n- `admin` plugin's `removeUser` (`packages/better-auth/src/plugins/admin/routes.ts:1463`).\n- `anonymous` plugin's self-delete endpoint (`packages/better-auth/src/plugins/anonymous/index.ts:222`).\n- `anonymous` plugin's after-link hook (`packages/better-auth/src/plugins/anonymous/index.ts:325`).\n- `@better-auth/scim`'s `DELETE /scim/v2/Users/:userId` (`packages/scim/src/routes.ts:1019`).\n\nWorking callers that already do the right thing: the core `/delete-user` self-delete and `/delete-user/callback` (`packages/better-auth/src/api/routes/update-user.ts:551`).\n\nThe fix shape extends each vulnerable caller to invoke `deleteSessions(userId)` before `deleteUser(userId)`. The architectural follow-up centralizes the cleanup inside `deleteUser` itself or introduces a single `deleteUserAndSessions` orchestrator so future callers cannot regress this contract.\n\n### Patches\n\nFixed in `better-auth@\u003cpatched-version\u003e` and `@better-auth/scim@\u003cpatched-version\u003e`. All four user-deletion call sites now invoke `deleteSessions(userId)` before `deleteUser(userId)` so sessions are evicted from secondary storage at the same time the user row is removed.\n\n### Workarounds\n\nIf users cannot upgrade immediately:\n\n- **Configuration-level**: set `session.storeSessionInDatabase: true`. Subsequent user-delete writes reach the session table and the database cascade removes rows. Increases write volume for high-throughput sessions but eliminates the gap.\n- **Code-level (admin path)**: when calling `auth.api.removeUser`, also call `auth.api.revokeUserSessions({ body: { userId } })`, which uses `deleteSessions` internally.\n- **Code-level (SCIM path)**: wrap their SCIM provider's deprovisioning hook to call `auth.api.revokeUserSessions(...)` after the SCIM DELETE.\n- **Code-level (anonymous path)**: in `onLinkAccount`, explicitly call `internalAdapter.deleteSessions(anonymousUser.user.id)` before allowing the new session to be issued.\n\n### Impact\n\n- **Stale session validity**: a deleted user's existing session cookie continues to authenticate against `getSessionFromCtx` until the session TTL elapses (default 7 days). Within that window, the deleted user retains their pre-existing read and write surface.\n- **SCIM-driven deprovisioning gap**: organizations using SCIM to revoke offboarded employees' access do not, in fact, revoke active sessions. The deleted account remains usable for up to 7 days after deprovisioning.\n\n### Credit\n\nReported by @iruizsalinas.\n\n### Resources\n\n- [CWE-613: Insufficient Session Expiration](https://cwe.mitre.org/data/definitions/613.html)\n- [CWE-672: Operation on a Resource after Expiration or Release](https://cwe.mitre.org/data/definitions/672.html)\n- [CWE-459: Incomplete Cleanup](https://cwe.mitre.org/data/definitions/459.html)","aliases":["CVE-2026-67334"],"modified":"2026-09-10T03:50:51.291924042Z","published":"2026-07-07T20:56:45Z","database_specific":{"nvd_published_at":null,"cwe_ids":["CWE-459","CWE-613","CWE-672"],"severity":"LOW","github_reviewed":true,"github_reviewed_at":"2026-07-07T20:56:45Z"},"references":[{"type":"WEB","url":"https://github.com/better-auth/better-auth/security/advisories/GHSA-2vg6-77g8-24mp"},{"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","ecosystem":"npm","purl":"pkg:npm/better-auth"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0.3.4"},{"fixed":"1.6.11"}]}],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/07/GHSA-2vg6-77g8-24mp/GHSA-2vg6-77g8-24mp.json"}},{"package":{"name":"@better-auth/scim","ecosystem":"npm","purl":"pkg:npm/%40better-auth/scim"},"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-2vg6-77g8-24mp/GHSA-2vg6-77g8-24mp.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:L/I:L/A:N"}]}