Your RSA-2048 keys break in 2030. Find every one of them before attackers do.
📦 npm

GHSA-2vg6-77g8-24mp

LOW

Better Auth: Stale sessions persist after user deletion across admin, anonymous, and SCIM flows

Published
Jul 7, 2026
Updated
Jul 7, 2026
Affected
2 pkgs
Patched
2 / 2
Exploits
None indexed

Blast Radius

2 pkgs affected
📦better-auth📦@better-auth/scim

Real-time download stats are indexed for npm and PyPI packages. This vulnerability affects npm packages — download data is not available via public APIs for these ecosystems.

Description

Am I affected?

Users are affected if all of the following are true:

  • They configure secondaryStorage on betterAuth(...) (Redis, KV, or any external session cache).
  • session.storeSessionInDatabase is left unset or set to false (the default).
  • Their application's deployment uses one or more of:
    • The admin plugin and calls auth.api.removeUser(...) or authClient.admin.removeUser(...).
    • The anonymous plugin and exposes /delete-anonymous-user or relies on the after-link hook to clean up the anonymous user.
    • The @better-auth/scim plugin and exposes DELETE /scim/v2/Users/:userId.

If storeSessionInDatabase is true, sessions are also written to the database, and the database delete cascades; users are not affected.

Fix:

  1. Upgrade to better-auth@<patched-version> or later (and @better-auth/scim@<patched-version> if they use SCIM).
  2. 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:

  • admin plugin's removeUser (packages/better-auth/src/plugins/admin/routes.ts:1463).
  • anonymous plugin's self-delete endpoint (packages/better-auth/src/plugins/anonymous/index.ts:222).
  • anonymous plugin's after-link hook (packages/better-auth/src/plugins/anonymous/index.ts:325).
  • @better-auth/scim's DELETE /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 call auth.api.revokeUserSessions({ body: { userId } }), which uses deleteSessions internally.
  • 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 call internalAdapter.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 getSessionFromCtx until 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 Packages

2 total 2 fixed
EcosystemPackageVulnerable rangeFix
📦npmbetter-auth0.3.4&&< 1.6.111.6.11
📦npm@better-auth/scim1.6.0&&< 1.6.111.6.11

Detection & mitigation playbook

Open-source dependency
  1. Detect

    Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for better-auth. O3's reachability analysis confirms whether the vulnerable code path is actually invoked in your application, so you act on real exposure instead of every transitive match.

  2. Fix

    Update better-auth to 1.6.11 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-2vg6-77g8-24mp is resolved across your whole dependency graph.

  3. Workarounds

    If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.

  4. How O3 protects you

    O3 pinpoints whether GHSA-2vg6-77g8-24mp is reachable in your code and exactly where to fix it, then blocks exploitation in production at runtime until the patched version is deployed.

Tailored to GHSA-2vg6-77g8-24mp. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.

Frequently Asked Questions

### Am I affected? Users are affected if all of the following are true: - They configure `secondaryStorage` on `betterAuth(...)` (Redis, KV, or any external session cache). - `session.storeSessionInDatabase` is left unset or set to `false` (the default). - Their application's deployment uses one or more of: - The `admin` plugin and calls `auth.api.removeUser(...)` or `authClient.admin.removeUser(...)`. - The `anonymous` plugin and exposes `/delete-anonymous-user` or relies on the after-link hook to clean up the anonymous user. - The `@better-auth/scim` plugin and exposes `DELETE /scim/
O3 Security · Impact-Aware SCA

Is GHSA-2vg6-77g8-24mp in your dependencies?

O3 detects GHSA-2vg6-77g8-24mp across npm dependencies and uses function-level reachability to confirm whether the vulnerable code path is actually reachable — not just present. No false positives.