GHSA-2vg6-77g8-24mp CVE-2026-67334
Better Auth: Stale sessions persist after user deletion across admin, anonymous, and SCIM flows
- Severity
- 5.1 / 10
- Medium · CVSS 4.0
- Exploit chance (EPSS)
- 0.32%
- Chance of exploitation in the next 30 days. Higher than 25.34% of scored vulnerabilities.
- Known exploited
- No
- Not in CISA's catalog
- Weakness
- CWE-459 Incomplete Cleanup
- CWE-613 Insufficient Session Expiration
- CWE-672 Operation on a Resource after Expiration or Release
- Fix
- Available
- 2 packages have a fixed version
Description
Am I affected?
Users are affected if all of the following are true:
- They configure
secondaryStorageonbetterAuth(...)(Redis, KV, or any external session cache). session.storeSessionInDatabaseis left unset or set tofalse(the default).- Their application's deployment uses one or more of:
- The
adminplugin and callsauth.api.removeUser(...)orauthClient.admin.removeUser(...). - The
anonymousplugin and exposes/delete-anonymous-useror relies on the after-link hook to clean up the anonymous user. - The
@better-auth/scimplugin and exposesDELETE /scim/v2/Users/:userId.
- The
If storeSessionInDatabase is true, sessions are also written to the database, and the database delete cascades; users are not affected.
Fix:
- Upgrade to
better-auth@<patched-version>or later (and@better-auth/scim@<patched-version>if they use SCIM). - If they cannot upgrade, see workarounds below.
Summary
When 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).
Details
The vulnerable call sites are:
adminplugin'sremoveUser(packages/better-auth/src/plugins/admin/routes.ts:1463).anonymousplugin's self-delete endpoint (packages/better-auth/src/plugins/anonymous/index.ts:222).anonymousplugin's after-link hook (packages/better-auth/src/plugins/anonymous/index.ts:325).@better-auth/scim'sDELETE /scim/v2/Users/:userId(packages/scim/src/routes.ts:1019).
Working 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).
The 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.
Patches
Fixed in better-auth@<patched-version> and @better-auth/scim@<patched-version>. 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.
Workarounds
If users cannot upgrade immediately:
- 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. - Code-level (admin path): when calling
auth.api.removeUser, also callauth.api.revokeUserSessions({ body: { userId } }), which usesdeleteSessionsinternally. - Code-level (SCIM path): wrap their SCIM provider's deprovisioning hook to call
auth.api.revokeUserSessions(...)after the SCIM DELETE. - Code-level (anonymous path): in
onLinkAccount, explicitly callinternalAdapter.deleteSessions(anonymousUser.user.id)before allowing the new session to be issued.
Impact
- Stale session validity: a deleted user's existing session cookie continues to authenticate against
getSessionFromCtxuntil the session TTL elapses (default 7 days). Within that window, the deleted user retains their pre-existing read and write surface. - 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.
Credit
Reported by @iruizsalinas.
Resources
Affected versions
npm
| Package | Vulnerable | Fixed in |
|---|---|---|
| @better-auth/scim | >= 1.6.0, < 1.6.11 |
1.6.11 |
| better-auth | >= 0.3.4, < 1.6.11 |
1.6.11 |
How to fix
Move to the fixed version or later with your package manager, update the lockfile, and run your tests. If the package only arrives through another dependency, upgrade that dependency first. If none of its releases pulls a fixed version yet, force the version with your package manager's override.
@better-auth/scim npm → 1.6.11
- npm
npm install @better-auth/scim@1.6.11- yarn
yarn add @better-auth/scim@1.6.11- pnpm
pnpm add @better-auth/scim@1.6.11
better-auth npm → 1.6.11
- npm
npm install better-auth@1.6.11- yarn
yarn add better-auth@1.6.11- pnpm
pnpm add better-auth@1.6.11
Severity breakdown
CVSS 4.0 5.1
CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X
- Attack vector
- Network
- Attack complexity
- Low
- Attack requirements
- None
- Privileges required
- High
- User interaction
- None
- Confidentiality (vulnerable system)
- Low
- Integrity (vulnerable system)
- Low
- Availability (vulnerable system)
- None
- Confidentiality (subsequent systems)
- None
- Integrity (subsequent systems)
- None
- Availability (subsequent systems)
- None
- Exploit maturity
- Not defined
- Scope
- X
CVSS 3.1 3.8
CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:L/I:L/A:N
- Attack vector
- Network
- Attack complexity
- Low
- Privileges required
- High
- User interaction
- None
- Scope
- Unchanged
- Confidentiality
- Low
- Integrity
- Low
- Availability
- None
Weakness
CWE-459: Incomplete Cleanup
The product does not properly "clean up" and remove temporary or supporting resources after they have been used.
How to prevent it
- Architecture and Design, Implementation: Temporary files and other supporting resources should be deleted/released immediately after they are no longer needed.
CWE-613: Insufficient Session Expiration
According to WASC, "Insufficient Session Expiration is when a web site permits an attacker to reuse old session credentials or session IDs for authorization."
How to prevent it
- Implementation: Set sessions/credentials expiration date.
CWE-672: Operation on a Resource after Expiration or Release
The product uses, accesses, or otherwise operates on a resource after that resource has been expired, released, or revoked.
References
Packages
Other links
Timeline
- Published
- Last updated by the source
- Latest EPSS score
Related advisories
| Advisory | Severity | Why it's related |
|---|---|---|
| GHSA-r4xp-prcw-77qf better-auth: OAuth Proxy lets an attacker sign in as another user | Critical | Also affects better-auth |
| GHSA-965c-763c-88jm better-auth: OAuth state can be used as a Magic Link to sign in as another user | Critical | Also affects better-auth |
| GHSA-44jh-23m7-hpcf @better-auth/drizzle-adapter: Concurrent requests can exceed rate limits on PostgreSQL | Medium | Also affects better-auth |
| GHSA-q84f-53jg-9ppm better-auth: Device login approval does not show the requesting client or permissions | High | Also affects better-auth |
| GHSA-qq9h-g4jm-xgf3 Better Auth: Account takeover via pre-account hijacking on magic-link and email-OTP sign-in | High | Also affects better-auth |
| GHSA-7w99-5wm4-3g79 @better-auth/oauth-provider's OAuth authorization-code grant allows concurrent redemption when two token requests race the find-then-delete primitive | High | Also affects better-auth |
| GHSA-392p-2q2v-4372 Better Auth: OAuth refresh-token rotation forks the token family on concurrent redemption | High | Also affects better-auth |
| GHSA-9h47-pqcx-hjr4 Better Auth has insecure cryptographic defaults in oidcProvider: alg=none advertised and plain PKCE accepted by default | Critical | Also affects better-auth |
MergeHarbor checks every repository's dependencies against this database, ranks what matters by exploitability and reachability, routes it to the owning team and opens the fix pull request.
Check your repos with MergeHarbor