Summary
The ordinary self-service account deletion/disable flow logs out only the current request. It does not invalidate the user's other active sessions, even though SessionInvalidationService.invalidateUserSessions() already exists and the GDPR deletion path uses it.
Severity: High
Affected code
src/main/java/com/digitalsanctuary/spring/user/api/UserAPI.java — deleteAccount (~389), logoutUser (~589): only SecurityContextHolder.clearContext() + request.logout() for the current request.
src/main/java/com/digitalsanctuary/spring/user/service/UserService.java — deleteOrDisableUser (~500): no session invalidation in either branch.
- Contrast:
src/main/java/com/digitalsanctuary/spring/user/api/GdprAPI.java:297 — the GDPR path does call invalidateUserSessions() ("invalidates ALL sessions across all devices").
Details
Default deletion is soft-delete (user.actuallyDeleteAccount=false), which sets enabled=false and saves. DSUserDetails caches authorities and the enabled flag at login time, and there is no per-request re-check against the database, so other established sessions remain fully authorized until natural expiry. The same holds for hard delete — a cached principal continues to authenticate after the backing row is removed. Sessions are already registered in the SessionRegistry (WebSecurityConfig.java:160), so revocation is available; the ordinary path simply doesn't invoke it.
Impact
A user who deletes or disables an account (e.g. because it was compromised) reasonably expects all sessions to end. Any other active session continues to have access until it expires (CWE-613).
Recommended fix
- Call
sessionInvalidationService.invalidateUserSessions(user) in the ordinary delete/disable flow, for both soft and hard deletion.
- Prefer placing revocation at the service layer or an after-commit listener so direct callers of
deleteOrDisableUser() cannot bypass it.
- Add a two-session integration test proving both soft-disable and hard-delete terminate the second session.
Identified during a code-first security assessment of the current main branch. The GDPR path already demonstrates the intended pattern.
Summary
The ordinary self-service account deletion/disable flow logs out only the current request. It does not invalidate the user's other active sessions, even though
SessionInvalidationService.invalidateUserSessions()already exists and the GDPR deletion path uses it.Severity: High
Affected code
src/main/java/com/digitalsanctuary/spring/user/api/UserAPI.java—deleteAccount(~389),logoutUser(~589): onlySecurityContextHolder.clearContext()+request.logout()for the current request.src/main/java/com/digitalsanctuary/spring/user/service/UserService.java—deleteOrDisableUser(~500): no session invalidation in either branch.src/main/java/com/digitalsanctuary/spring/user/api/GdprAPI.java:297— the GDPR path does callinvalidateUserSessions()("invalidates ALL sessions across all devices").Details
Default deletion is soft-delete (
user.actuallyDeleteAccount=false), which setsenabled=falseand saves.DSUserDetailscaches authorities and theenabledflag at login time, and there is no per-request re-check against the database, so other established sessions remain fully authorized until natural expiry. The same holds for hard delete — a cached principal continues to authenticate after the backing row is removed. Sessions are already registered in theSessionRegistry(WebSecurityConfig.java:160), so revocation is available; the ordinary path simply doesn't invoke it.Impact
A user who deletes or disables an account (e.g. because it was compromised) reasonably expects all sessions to end. Any other active session continues to have access until it expires (CWE-613).
Recommended fix
sessionInvalidationService.invalidateUserSessions(user)in the ordinary delete/disable flow, for both soft and hard deletion.deleteOrDisableUser()cannot bypass it.Identified during a code-first security assessment of the current
mainbranch. The GDPR path already demonstrates the intended pattern.