From ca7f718353673bbbf5ffa15d0d6fae4bdd342d24 Mon Sep 17 00:00:00 2001 From: sebastien Date: Tue, 5 Nov 2024 15:43:15 +0100 Subject: [PATCH] adds mail sending to admin if failure of api HIBP request to check if password is compromised --- canaille/app/forms.py | 29 +++++-- canaille/core/endpoints/account.py | 4 +- canaille/core/endpoints/forms.py | 4 +- canaille/core/mails.py | 40 +++++++++ .../mails/pwned-password-non-checked.html | 81 +++++++++++++++++++ .../mails/pwned-password-non-checked.txt | 14 ++++ 6 files changed, 161 insertions(+), 11 deletions(-) create mode 100644 canaille/core/templates/mails/pwned-password-non-checked.html create mode 100644 canaille/core/templates/mails/pwned-password-non-checked.txt diff --git a/canaille/app/forms.py b/canaille/app/forms.py index 96c1d943..d010a031 100644 --- a/canaille/app/forms.py +++ b/canaille/app/forms.py @@ -16,6 +16,7 @@ from canaille.app.i18n import gettext as _ from canaille.app.i18n import locale_selector from canaille.app.i18n import timezone_selector from canaille.backends import Backend +from canaille.core.mails import send_comprimised_password_check_failure_mail from . import validate_uri from .flask import request_is_htmx @@ -84,7 +85,7 @@ def password_strength_calculator(password): return strength_score -def pwned_password_validator(form, field): +def compromised_password_validator(form, field): try: from hashlib import sha1 @@ -94,18 +95,32 @@ def pwned_password_validator(form, field): hashed_password = sha1(field.data.encode("utf-8")).hexdigest() hashed_password_splited = (hashed_password[:5].upper(), hashed_password[5:].upper()) + + api_url = f"https://api.pwnedpasswords.com/range/{hashed_password_splited[0]}" + try: - response = requests.api.get( - f"https://api.pwnedpasswords.com/range/{hashed_password_splited[0]}", - timeout=10, - ) - except requests.exceptions.HTTPError as e: + response = requests.api.get(api_url, timeout=10) + except Exception as e: print("Error: " + str(e)) + if current_app.features.has_smtp and not request_is_htmx(): + if form.user is not None: + user_name = form.user.user_name + user_email = form.user.emails[0] + else: + user_name = form["user_name"].data + user_email = form["emails"].data[0] + + send_comprimised_password_check_failure_mail( + api_url, user_name, user_email, hashed_password + ) + + return None + decoded_response = response.content.decode("utf8").split("\r\n") for each in decoded_response: - if hashed_password_splited[1] == each.split(":")[0]: + if hashed_password_splited[1] in each.split(":")[0]: raise wtforms.ValidationError(_("This password is compromised.")) diff --git a/canaille/core/endpoints/account.py b/canaille/core/endpoints/account.py index 4437c4a3..af8868d3 100644 --- a/canaille/core/endpoints/account.py +++ b/canaille/core/endpoints/account.py @@ -34,10 +34,10 @@ from canaille.app.flask import smtp_needed from canaille.app.flask import user_needed from canaille.app.forms import IDToModel from canaille.app.forms import TableForm +from canaille.app.forms import compromised_password_validator from canaille.app.forms import is_readonly from canaille.app.forms import password_length_validator from canaille.app.forms import password_too_long_validator -from canaille.app.forms import pwned_password_validator from canaille.app.forms import set_readonly from canaille.app.forms import set_writable from canaille.app.i18n import gettext as _ @@ -317,7 +317,7 @@ def registration(data=None, hash=None): wtforms.validators.DataRequired(), password_length_validator, password_too_long_validator, - pwned_password_validator, + compromised_password_validator, ] form["password2"].validators = [ wtforms.validators.DataRequired(), diff --git a/canaille/core/endpoints/forms.py b/canaille/core/endpoints/forms.py index f3a6c577..5b3bddc3 100644 --- a/canaille/core/endpoints/forms.py +++ b/canaille/core/endpoints/forms.py @@ -10,12 +10,12 @@ from canaille.app.forms import BaseForm from canaille.app.forms import DateTimeUTCField from canaille.app.forms import Form from canaille.app.forms import IDToModel +from canaille.app.forms import compromised_password_validator from canaille.app.forms import email_validator from canaille.app.forms import is_uri from canaille.app.forms import password_length_validator from canaille.app.forms import password_too_long_validator from canaille.app.forms import phone_number -from canaille.app.forms import pwned_password_validator from canaille.app.forms import set_readonly from canaille.app.forms import unique_values from canaille.app.i18n import gettext @@ -266,7 +266,7 @@ PROFILE_FORM_FIELDS = dict( wtforms.validators.Optional(), password_length_validator, password_too_long_validator, - pwned_password_validator, + compromised_password_validator, ], render_kw={ "autocomplete": "new-password", diff --git a/canaille/core/mails.py b/canaille/core/mails.py index 364b8694..f184af64 100644 --- a/canaille/core/mails.py +++ b/canaille/core/mails.py @@ -210,3 +210,43 @@ def send_registration_mail(email, registration_url): html=html_body, attachments=[(logo_cid, logo_filename, logo_raw)] if logo_filename else None, ) + + +def send_comprimised_password_check_failure_mail( + check_password_url, user_name, user_email, hashed_password +): + base_url = url_for("core.account.index", _external=True) + logo_cid, logo_filename, logo_raw = logo() + + subject = _("Pwned password check incident on {website_name}").format( + website_name=current_app.config["CANAILLE"]["NAME"] + ) + text_body = render_template( + "mails/pwned-password-non-checked.txt", + site_name=current_app.config["CANAILLE"]["NAME"], + site_url=base_url, + check_password_url=check_password_url, + user_name=user_name, + user_email=user_email, + hashed_password=hashed_password, + ) + html_body = render_template( + "mails/pwned-password-non-checked.html", + site_name=current_app.config["CANAILLE"]["NAME"], + site_url=base_url, + check_password_url=check_password_url, + logo=f"cid:{logo_cid[1:-1]}" if logo_cid else None, + title=subject, + user_name=user_name, + user_email=user_email, + hashed_password=hashed_password, + ) + + return send_email( + subject=subject, + # line to change with admin group mails.... + recipient="sebastien@yaal.coop", + text=text_body, + html=html_body, + attachments=[(logo_cid, logo_filename, logo_raw)] if logo_filename else None, + ) diff --git a/canaille/core/templates/mails/pwned-password-non-checked.html b/canaille/core/templates/mails/pwned-password-non-checked.html new file mode 100644 index 00000000..1fc93ba7 --- /dev/null +++ b/canaille/core/templates/mails/pwned-password-non-checked.html @@ -0,0 +1,81 @@ + + + + + + + {{ title }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

+ {% if logo %} + {{ site_name }} + {% endif %} +
+ {% trans %}Verification failed if password is compromised.{% endtrans %} +
+

+
+

+ {% trans %}Our services were unable to verify if the {{ user_name }}'s password is compromised.{% endtrans %}
+

+

+ {% trans %}You have to check manually if the new password of the user {{ user_name }} is compromised.{% endtrans %}
+ {% trans %}Follow this steps : {% endtrans %}
+ {% trans %}1. click on the link above "Check if password is pwned"{% endtrans %}
+

+
+ {% trans %}Check if password is compromised{% endtrans %} +
+

+ {% trans %}2. in the page that will open, search the following hashed password in the page : {{ hashed_password }}{% endtrans %}
+ {% trans %}3. if the password is in the list :{% endtrans %}
+ {% trans %}3.1. open this link and reset user's password. {% endtrans %}
+

+
+ {% trans %} Reset {{ user_name }}'s password {% endtrans %} +
+

+ {% trans %}3.2. send an email to the user to explain the situation : {{ user_email }}{% endtrans %}
+

+
+ {{ site_name }} + + {% trans %}Check if password is compromised{% endtrans %} +
+ + diff --git a/canaille/core/templates/mails/pwned-password-non-checked.txt b/canaille/core/templates/mails/pwned-password-non-checked.txt new file mode 100644 index 00000000..a59800aa --- /dev/null +++ b/canaille/core/templates/mails/pwned-password-non-checked.txt @@ -0,0 +1,14 @@ +# {% trans %}Verification failed if password is compromised.{% endtrans %} + +{% trans %}Our services were unable to verify if the http://127.0.0.1:5000/profile/{{ user_name }}/settings's password is compromised.{% endtrans %} + +{% trans %}You have to check manually if the new password of the user {{ user_name }} is compromised.{% endtrans %} +{% trans %}Follow this steps : {% endtrans %} +{% trans %}1. click on the link above "Check if password is pwned"{% endtrans %} +{% trans %}2. in the page that will open, search the following hashed password in the page : {{ hashed_password }}{% endtrans %} +{% trans %}3. if the password is in the list :{% endtrans %} +{% trans %}3.1. open this link http://127.0.0.1:5000/profile/{{ user_name }}/settings and reset user's password.{% endtrans %} +{% trans %}3.2. send an email to the user to explain the situation : {{ user_email }}.{% endtrans %} + +{% trans %}Check if password is compromised{% endtrans %}: {{ check_password_url }} +{{ site_name }}: {{ site_url }}