From f00c4b77384967641b7abf7a91ccf06d87a6cf55 Mon Sep 17 00:00:00 2001 From: shariquerik Date: Tue, 14 Apr 2026 15:23:04 +0530 Subject: [PATCH] fix: enhance password reset flow to prevent username enumeration --- frappe/core/doctype/user/user.py | 30 ++++++++++++------------ frappe/templates/includes/login/login.js | 14 +++-------- 2 files changed, 18 insertions(+), 26 deletions(-) diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index 5f4b1d3fae..d1616b3831 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -1127,24 +1127,24 @@ def sign_up(email: str, full_name: str, redirect_to: str) -> tuple[int, str]: @frappe.whitelist(allow_guest=True, methods=["POST"]) @rate_limit(limit=get_password_reset_limit, seconds=60 * 60) def reset_password(user: str) -> str: + # Always return the same generic response regardless of whether the user + # exists, is disabled, or is restricted. This prevents username enumeration + # via different messages or HTTP status codes (CWE-204). + try: - user: User = frappe.get_doc("User", user) - if user.name == "Administrator": - return "not allowed" - if not user.enabled: - return "disabled" - - user.validate_reset_password() - user.reset_password(send_email=True) - - return frappe.msgprint( - msg=_("Password reset instructions have been sent to {}'s email").format(user.full_name), - title=_("Password Email Sent"), - ) + user_doc: User = frappe.get_doc("User", user) + if user_doc.name != "Administrator" and user_doc.enabled: + user_doc.validate_reset_password() + user_doc.reset_password(send_email=True) + # For Administrator or disabled users: silently skip — same response below except frappe.DoesNotExistError: - frappe.local.response["http_status_code"] = 404 frappe.clear_messages() - return "not found" + # Do not reveal whether the account exists — fall through to generic response + + return frappe.msgprint( + msg=_("If an account with this email exists, password reset instructions have been sent."), + title=_("Password Reset"), + ) @frappe.whitelist() diff --git a/frappe/templates/includes/login/login.js b/frappe/templates/includes/login/login.js index acaafa1199..c039c2351a 100644 --- a/frappe/templates/includes/login/login.js +++ b/frappe/templates/includes/login/login.js @@ -247,17 +247,9 @@ login.login_handlers = (function () { window.location.href = data.home_page; } } else if (window.location.hash === '#forgot') { - if (data.message === 'not found') { - login.set_status({{ _("Not a valid user") | tojson }}, 'red'); - } else if (data.message == 'not allowed') { - login.set_status({{ _("Not Allowed") | tojson }}, 'red'); - } else if (data.message == 'disabled') { - login.set_status({{ _("Not Allowed: Disabled User") | tojson }}, 'red'); - } else { - login.set_status({{ _("Instructions Emailed") | tojson }}, 'green'); - } - - + // Always show the same message regardless of whether the account + // exists or not, to prevent username enumeration (CWE-204). + login.set_status({{ _("Instructions Emailed") | tojson }}, 'green'); } else if (window.location.hash === '#signup') { if (cint(data.message[0]) == 0) { login.set_status(data.message[1], 'red');