From e755397243405fd0df71160ba6e8a76c14402c57 Mon Sep 17 00:00:00 2001 From: Yann Weber Date: Wed, 7 May 2025 17:55:49 +0200 Subject: [PATCH 1/3] add failing test on user_inputs side effects --- tests/zxcvbn_test.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/zxcvbn_test.py b/tests/zxcvbn_test.py index 6752c1f..a33c8dd 100644 --- a/tests/zxcvbn_test.py +++ b/tests/zxcvbn_test.py @@ -46,3 +46,13 @@ def test_empty_password(): zxcvbn(password, user_inputs=[input_]) except IndexError as ie: assert False, "Empty password raised IndexError" + + +def test_user_inputs_side_effects(): + password = '7r3iz3|)0uz3' + input_ = [password] + + guess1 = zxcvbn(password)['guesses_log10'] + zxcvbn('somepassword', user_inputs=input_) + guess2 = zxcvbn(password)['guesses_log10'] + assert abs(guess1 - guess2) < 1 From aa9ed644db7354fd137bdf0bd43bdda05f58f591 Mon Sep 17 00:00:00 2001 From: Yann Weber Date: Wed, 7 May 2025 18:06:21 +0200 Subject: [PATCH 2/3] Clear _ranked_dictionaries['user_inputs'] when no user_inputs given --- zxcvbn/matching.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/zxcvbn/matching.py b/zxcvbn/matching.py index e211ab3..7845aaa 100644 --- a/zxcvbn/matching.py +++ b/zxcvbn/matching.py @@ -100,6 +100,8 @@ def wrapper(*args, **kwargs): def omnimatch(password, _ranked_dictionaries=None, user_inputs=[]): if len(user_inputs): _ranked_dictionaries['user_inputs'] = build_ranked_dict(user_inputs) + elif 'user_inputs' in _ranked_dictionaries: + del(_ranked_dictionaries['user_inputs']) matches = [] for matcher in [ From c4951d47633010db52125eafcf505be7c61890cd Mon Sep 17 00:00:00 2001 From: mfgm2 Date: Thu, 12 Jun 2025 21:35:40 +0400 Subject: [PATCH 3/3] use shallow copy for ranked dicts when user_inputs are present --- zxcvbn/matching.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/zxcvbn/matching.py b/zxcvbn/matching.py index 7845aaa..1f43349 100644 --- a/zxcvbn/matching.py +++ b/zxcvbn/matching.py @@ -2,6 +2,7 @@ from . import adjacency_graphs import re import functools +import copy from zxcvbn.scoring import most_guessable_match_sequence @@ -98,10 +99,9 @@ def wrapper(*args, **kwargs): # omnimatch -- perform all matches @ensure_ranked_dictionaries def omnimatch(password, _ranked_dictionaries=None, user_inputs=[]): - if len(user_inputs): + if _ranked_dictionaries is not None and user_inputs: + _ranked_dictionaries = copy.copy(_ranked_dictionaries) _ranked_dictionaries['user_inputs'] = build_ranked_dict(user_inputs) - elif 'user_inputs' in _ranked_dictionaries: - del(_ranked_dictionaries['user_inputs']) matches = [] for matcher in [