Handle sessions on password change correctly

remove_user_sessions() got the user id as except_id, so the exception
never matched. On an own password change the result was correct only
by accident. When an admin changed another user's password, the admin
session got a new id, but the old entry stayed valid in the store.

- New Session.regenerate(): keeps the data under a new id and removes
  the old store entry.
- change_password() handles the sessions: on an own password change it
  regenerates the current session and ends all other sessions of the
  user (except_id is now the session id). When an admin changes another
  user's password, the admin session stays untouched and all sessions
  of that user end.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
finnandClaude Opus 5.5 committed 2026-10-21 17:48:03 +00:00
1 parent 9aed6b5227
commit 7ac4e6c3a7
3 files changed
+15 -4

No files matched your search

+8 -1
View File
@@ -6,6 +6,7 @@ from flask import session
from max.crypto import constant_time_compare, force_bytes from max.crypto import constant_time_compare, force_bytes
from max.db import get_user_by_email, update_user from max.db import get_user_by_email, update_user
from max.sessions import session_storage
from max.translations import t from max.translations import t
SSHA512_PREFIX = "{SSHA512}" SSHA512_PREFIX = "{SSHA512}"
@@ -35,7 +36,13 @@ def logout():
def change_password(user, password): def change_password(user, password):
hash = hash_password(password) hash = hash_password(password)
update_user(user.id, passwordhash=hash) update_user(user.id, passwordhash=hash)
session.set_new_id() if session.get("user_id") == user.id:
# Own password: keep this session (with a new id), end all others
session.regenerate()
session_storage.remove_user_sessions(user.id, except_id=session.id)
else:
# An admin changes the password of another user: end all of their sessions
session_storage.remove_user_sessions(user.id)
def hash_password(password: str, salt: bytes = None): def hash_password(password: str, salt: bytes = None):
+1 -3
View File
@@ -1,8 +1,7 @@
from flask import flash, session from flask import flash
from max.auth import change_password from max.auth import change_password
from max.permissions import AllowAdminOrSelf from max.permissions import AllowAdminOrSelf
from max.sessions import session_storage
from max.translations import t from max.translations import t
from ..base_user_views import BackToUsersMixin, FetchUserTemplateGetView from ..base_user_views import BackToUsersMixin, FetchUserTemplateGetView
@@ -28,6 +27,5 @@ class UserChangePassword(AllowAdminOrSelf, BackToUsersMixin, FetchUserTemplateGe
return return
change_password(self.user, pw1) change_password(self.user, pw1)
session_storage.remove_user_sessions(self.user.id, except_id=session["user_id"])
flash(t("Password changed successfully!"), category="success") flash(t("Password changed successfully!"), category="success")
return self.redirect() return self.redirect()
+6
View File
@@ -91,6 +91,12 @@ class Session(CallbackDict, SessionMixin):
def set_new_id(self): def set_new_id(self):
self.id = get_random_string(32) self.id = get_random_string(32)
def regenerate(self):
"""Keeps the data under a new id. The old id becomes invalid."""
del session_storage[self.id]
self.set_new_id()
self.modified = True
class SessionInterface(FlaskSessionInterface): class SessionInterface(FlaskSessionInterface):
def _get_signer(self, app): def _get_signer(self, app):