diff options
| author | Florian Apolloner <florian@apolloner.eu> | 2021-12-27 14:48:03 +0100 |
|---|---|---|
| committer | Carlton Gibson <carlton.gibson@noumenal.es> | 2022-01-04 10:02:05 +0100 |
| commit | 968a3d01fa79f055f93a1c3ed1535ecbcbdbb842 (patch) | |
| tree | ac977466ff6d3ae6daf70389f7895aad4c4e5981 /django/contrib/auth/password_validation.py | |
| parent | ccafad2e429468c518c80fb178f9e7a3f06e78e1 (diff) | |
Fixed CVE-2021-45115 -- Prevented DoS vector in UserAttributeSimilarityValidator.
Thanks Chris Bailey for the report.
Co-authored-by: Adam Johnson <me@adamj.eu>
Diffstat (limited to 'django/contrib/auth/password_validation.py')
| -rw-r--r-- | django/contrib/auth/password_validation.py | 40 |
1 files changed, 38 insertions, 2 deletions
diff --git a/django/contrib/auth/password_validation.py b/django/contrib/auth/password_validation.py index bc70a84543..2e64bf1586 100644 --- a/django/contrib/auth/password_validation.py +++ b/django/contrib/auth/password_validation.py @@ -115,6 +115,36 @@ class MinimumLengthValidator: ) % {'min_length': self.min_length} +def exceeds_maximum_length_ratio(password, max_similarity, value): + """ + Test that value is within a reasonable range of password. + + The following ratio calculations are based on testing SequenceMatcher like + this: + + for i in range(0,6): + print(10**i, SequenceMatcher(a='A', b='A'*(10**i)).quick_ratio()) + + which yields: + + 1 1.0 + 10 0.18181818181818182 + 100 0.019801980198019802 + 1000 0.001998001998001998 + 10000 0.00019998000199980003 + 100000 1.999980000199998e-05 + + This means a length_ratio of 10 should never yield a similarity higher than + 0.2, for 100 this is down to 0.02 and for 1000 it is 0.002. This can be + calculated via 2 / length_ratio. As a result we avoid the potentially + expensive sequence matching. + """ + pwd_len = len(password) + length_bound_similarity = max_similarity / 2 * pwd_len + value_len = len(value) + return pwd_len >= 10 * value_len and value_len < length_bound_similarity + + class UserAttributeSimilarityValidator: """ Validate that the password is sufficiently different from the user's @@ -130,19 +160,25 @@ class UserAttributeSimilarityValidator: def __init__(self, user_attributes=DEFAULT_USER_ATTRIBUTES, max_similarity=0.7): self.user_attributes = user_attributes + if max_similarity < 0.1: + raise ValueError('max_similarity must be at least 0.1') self.max_similarity = max_similarity def validate(self, password, user=None): if not user: return + password = password.lower() for attribute_name in self.user_attributes: value = getattr(user, attribute_name, None) if not value or not isinstance(value, str): continue - value_parts = re.split(r'\W+', value) + [value] + value_lower = value.lower() + value_parts = re.split(r'\W+', value_lower) + [value_lower] for value_part in value_parts: - if SequenceMatcher(a=password.lower(), b=value_part.lower()).quick_ratio() >= self.max_similarity: + if exceeds_maximum_length_ratio(password, self.max_similarity, value_part): + continue + if SequenceMatcher(a=password, b=value_part).quick_ratio() >= self.max_similarity: try: verbose_name = str(user._meta.get_field(attribute_name).verbose_name) except FieldDoesNotExist: |
