Ask for the current password when changing the own password

A hijacked session was enough to change the own password, lock out the
owner and get permanent access, including the mailbox via Dovecot.

Changing the own password now requires the current password, for users
and admins alike. Admins changing the password of another user are not
asked, since an admin session can take over accounts in other ways
anyway. The new password fields are marked as new-password for password
managers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
finnandClaude Opus 5.5 committed 2026-10-21 19:38:14 +00:00
1 parent 6e6ae3ccb7
commit 58242117f6
3 files changed
+33 -4

No files matched your search

+3
View File
@@ -51,6 +51,9 @@
"Note changed successfully!": "Notiz wurde erfolgreich bearbeitet!", "Note changed successfully!": "Notiz wurde erfolgreich bearbeitet!",
"Both passwords must be given": "Beide Passwörter müssen gegeben sein", "Both passwords must be given": "Beide Passwörter müssen gegeben sein",
"Both passwords must be the same": "Beide Passwörter müssen übereinstimmen", "Both passwords must be the same": "Beide Passwörter müssen übereinstimmen",
"Current password": "Aktuelles Passwort",
"The current password must be given": "Das aktuelle Passwort muss gegeben sein",
"The current password is wrong": "Das aktuelle Passwort ist falsch",
"Password changed successfully!": "Passwort erfolgreich geändert!", "Password changed successfully!": "Passwort erfolgreich geändert!",
"Admin is not set": "Admin ist nicht gesetzt", "Admin is not set": "Admin ist nicht gesetzt",
"Creation of {email} was successful!": "{email} wurde erfolgreich erstellt!", "Creation of {email} was successful!": "{email} wurde erfolgreich erstellt!",
+21 -2
View File
@@ -1,6 +1,6 @@
from flask import flash from flask import flash
from max.auth import change_password from max.auth import change_password, check_password
from max.permissions import AllowAdminOrSelf from max.permissions import AllowAdminOrSelf
from max.translations import t from max.translations import t
@@ -13,15 +13,34 @@ class UserChangePassword(AllowAdminOrSelf, BackToUsersMixin, FetchUserTemplateGe
def get_webpagetitle(self): def get_webpagetitle(self):
return t("Change password for {user}", user=self.user.email) return t("Change password for {user}", user=self.user.email)
def is_own_password(self):
return self.user.id == self.auth_user.id
def get_context(self):
context = super().get_context()
context["is_own_password"] = self.is_own_password()
return context
def post(self): def post(self):
pw1 = self.request.form.get("password1") pw1 = self.request.form.get("password1")
pw2 = self.request.form.get("password2") pw2 = self.request.form.get("password2")
if not pw1 or not pw2: if not pw1 or not pw2:
flash(t("Both passwords must be given"), category="error") flash(t("Both passwords must be given"), category="error")
return return
if not isinstance(pw1, str) or not isinstance(pw2, str): if not isinstance(pw1, str) or not isinstance(pw2, str):
flash("Invalid data") flash("Invalid data", category="error")
return return
if self.is_own_password():
current_password = self.request.form.get("current_password")
if not current_password or not isinstance(current_password, str):
flash(t("The current password must be given"), category="error")
return
if not check_password(current_password, self.user.passwordhash):
flash(t("The current password is wrong"), category="error")
return
if pw1 != pw2: if pw1 != pw2:
flash(t("Both passwords must be the same"), category="error") flash(t("Both passwords must be the same"), category="error")
return return
+9 -2
View File
@@ -5,14 +5,21 @@
{% include "alerts.html" %} {% include "alerts.html" %}
<form class="pure-form pure-form-aligned" action="{{ url_for('user-change-password', user_id=user.id, return=return_to) }}" method="post"> <form class="pure-form pure-form-aligned" action="{{ url_for('user-change-password', user_id=user.id, return=return_to) }}" method="post">
{% if is_own_password %}
<div class="pure-control-group">
<label for="current_password">{{ t("Current password") }}</label>
<input id="current_password" type="password" name="current_password" autocomplete="current-password" placeholder="{{ t("Current password") }}">
</div>
{% endif %}
<div class="pure-control-group"> <div class="pure-control-group">
<label for="password1">{{ t("Password") }}</label> <label for="password1">{{ t("Password") }}</label>
<input id="password1" type="password" name="password1" placeholder="{{ t("New password") }}"> <input id="password1" type="password" name="password1" autocomplete="new-password" placeholder="{{ t("New password") }}">
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="password2">{{ t("Reenter Password") }}</label> <label for="password2">{{ t("Reenter Password") }}</label>
<input id="password2" type="password" name="password2" placeholder="{{ t("New password") }}"> <input id="password2" type="password" name="password2" autocomplete="new-password" placeholder="{{ t("New password") }}">
</div> </div>
<div class="pure-controls"> <div class="pure-controls">