[2.2.x] Fixed CVE-2021-45115 -- Prevented DoS vector in UserAttributeSimilarityValidator.

Thanks Chris Bailey for the report.

Co-authored-by: Adam Johnson <me@adamj.eu>
This commit is contained in:
Florian Apolloner 2021-12-27 14:48:03 +01:00 committed by Carlton Gibson
parent 03b733d8a8
commit 2135637fdd
4 changed files with 65 additions and 14 deletions

View File

@ -115,6 +115,36 @@ class MinimumLengthValidator:
) % {'min_length': self.min_length} ) % {'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: class UserAttributeSimilarityValidator:
""" """
Validate whether the password is sufficiently different from the user's Validate whether 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): def __init__(self, user_attributes=DEFAULT_USER_ATTRIBUTES, max_similarity=0.7):
self.user_attributes = user_attributes 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 self.max_similarity = max_similarity
def validate(self, password, user=None): def validate(self, password, user=None):
if not user: if not user:
return return
password = password.lower()
for attribute_name in self.user_attributes: for attribute_name in self.user_attributes:
value = getattr(user, attribute_name, None) value = getattr(user, attribute_name, None)
if not value or not isinstance(value, str): if not value or not isinstance(value, str):
continue 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: 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: try:
verbose_name = str(user._meta.get_field(attribute_name).verbose_name) verbose_name = str(user._meta.get_field(attribute_name).verbose_name)
except FieldDoesNotExist: except FieldDoesNotExist:

View File

@ -7,4 +7,16 @@ Django 2.2.26 release notes
Django 2.2.26 fixes one security issue with severity "medium" and two security Django 2.2.26 fixes one security issue with severity "medium" and two security
issues with severity "low" in 2.2.25. issues with severity "low" in 2.2.25.
... CVE-2021-45115: Denial-of-service possibility in ``UserAttributeSimilarityValidator``
=====================================================================================
:class:`.UserAttributeSimilarityValidator` incurred significant overhead
evaluating submitted password that were artificially large in relative to the
comparison values. On the assumption that access to user registration was
unrestricted this provided a potential vector for a denial-of-service attack.
In order to mitigate this issue, relatively long values are now ignored by
``UserAttributeSimilarityValidator``.
This issue has severity "medium" according to the :ref:`Django security policy
<security-disclosure>`.

View File

@ -522,10 +522,16 @@ Django includes four validators:
is used: ``'username', 'first_name', 'last_name', 'email'``. is used: ``'username', 'first_name', 'last_name', 'email'``.
Attributes that don't exist are ignored. Attributes that don't exist are ignored.
The minimum similarity of a rejected password can be set on a scale of 0 to The maximum allowed similarity of passwords can be set on a scale of 0.1
1 with the ``max_similarity`` parameter. A setting of 0 rejects all to 1.0 with the ``max_similarity`` parameter. This is compared to the
passwords, whereas a setting of 1 rejects only passwords that are identical result of :meth:`difflib.SequenceMatcher.quick_ratio`. A value of 0.1
to an attribute's value. rejects passwords unless they are substantially different from the
``user_attributes``, whereas a value of 1.0 rejects only passwords that are
identical to an attribute's value.
.. versionchanged:: 2.2.26
The ``max_similarity`` parameter was limited to a minimum value of 0.1.
.. class:: CommonPasswordValidator(password_list_path=DEFAULT_PASSWORD_LIST_PATH) .. class:: CommonPasswordValidator(password_list_path=DEFAULT_PASSWORD_LIST_PATH)

View File

@ -150,13 +150,10 @@ class UserAttributeSimilarityValidatorTest(TestCase):
max_similarity=1, max_similarity=1,
).validate(user.first_name, user=user) ).validate(user.first_name, user=user)
self.assertEqual(cm.exception.messages, [expected_error % "first name"]) self.assertEqual(cm.exception.messages, [expected_error % "first name"])
# max_similarity=0 rejects all passwords. # Very low max_similarity is rejected.
with self.assertRaises(ValidationError) as cm: msg = 'max_similarity must be at least 0.1'
UserAttributeSimilarityValidator( with self.assertRaisesMessage(ValueError, msg):
user_attributes=['first_name'], UserAttributeSimilarityValidator(max_similarity=0.09)
max_similarity=0,
).validate('XXX', user=user)
self.assertEqual(cm.exception.messages, [expected_error % "first name"])
# Passes validation. # Passes validation.
self.assertIsNone( self.assertIsNone(
UserAttributeSimilarityValidator(user_attributes=['first_name']).validate('testclient', user=user) UserAttributeSimilarityValidator(user_attributes=['first_name']).validate('testclient', user=user)