From 7ac4e6c3a7a61cab21f1adee903b00a8750c66e7 Mon Sep 17 00:00:00 2001 From: Finn Stutzenstein Date: Wed, 21 Oct 2026 17:48:03 +0000 Subject: [PATCH] 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 --- max/auth/auth.py | 9 ++++++++- max/routes/user/change_password.py | 4 +--- max/sessions.py | 6 ++++++ 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/max/auth/auth.py b/max/auth/auth.py index 46adc1e..da1e353 100644 --- a/max/auth/auth.py +++ b/max/auth/auth.py @@ -6,6 +6,7 @@ from flask import session from max.crypto import constant_time_compare, force_bytes from max.db import get_user_by_email, update_user +from max.sessions import session_storage from max.translations import t SSHA512_PREFIX = "{SSHA512}" @@ -35,7 +36,13 @@ def logout(): def change_password(user, password): hash = hash_password(password) 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): diff --git a/max/routes/user/change_password.py b/max/routes/user/change_password.py index bbda519..e481a92 100644 --- a/max/routes/user/change_password.py +++ b/max/routes/user/change_password.py @@ -1,8 +1,7 @@ -from flask import flash, session +from flask import flash from max.auth import change_password from max.permissions import AllowAdminOrSelf -from max.sessions import session_storage from max.translations import t from ..base_user_views import BackToUsersMixin, FetchUserTemplateGetView @@ -28,6 +27,5 @@ class UserChangePassword(AllowAdminOrSelf, BackToUsersMixin, FetchUserTemplateGe return 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") return self.redirect() diff --git a/max/sessions.py b/max/sessions.py index 1e02157..166172f 100644 --- a/max/sessions.py +++ b/max/sessions.py @@ -91,6 +91,12 @@ class Session(CallbackDict, SessionMixin): def set_new_id(self): 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): def _get_signer(self, app):