CVE-2021-45115
Description
An issue was discovered in Django 2.2 before 2.2.26, 3.2 before 3.2.11, and 4.0 before 4.0.1. UserAttributeSimilarityValidator incurred significant overhead in evaluating a submitted password that was artificially large in relation to the comparison values. In a situation where access to user registration was unrestricted, this provided a potential vector for a denial-of-service attack.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Django's UserAttributeSimilarityValidator allows denial of service via crafted oversized passwords.
Vulnerability
The UserAttributeSimilarityValidator in Django, a password validator that checks whether a password is sufficiently different from the user's attributes (e.g., username, email), incurred significant processing overhead when comparing a submitted password that was artificially large in relation to the comparison values. The issue is present in Django versions 2.2 before 2.2.26, 3.2 before 3.2.11, and 4.0 before 4.0.1 [1][2]. The validator used Python's SequenceMatcher to compute similarity ratios without a length-based guard; for extremely long passwords compared to short user attribute values, SequenceMatcher performed expensive operations, leading to excessive CPU consumption [3][4].
Exploitation
An attacker with network access to a registration endpoint that uses UserAttributeSimilarityValidator can submit a password that is many times longer than a typical user attribute value (e.g., username of 10 characters versus a password of 10,000 characters). The exploit does not require authentication or special privileges. No user interaction is needed beyond the attacker submitting the crafted password via the registration form [1][2]. The commit diffs show that the fix adds an early return when the password length exceeds 10 times the value length and the value length is below a calculated bound based on the similarity threshold [3][4].
Impact
Successful exploitation results in a denial-of-service (DoS) condition on the targeted web server. The attacker can tie up server resources by causing the validator to waste CPU time on expensive sequence matching, potentially making the application unresponsive for other users or exhausting server capacity [1][2].
Mitigation
The vulnerability is fixed in Django 2.2.26, 3.2.11, and 4.0.1, which were released on 2022-01-04 [1][2]. Upgrading to these versions or later eliminates the DoS vector. No known workarounds are documented; the fix introduces the exceeds_maximum_length_ratio function to short-circuit evaluation when the length ratio is too large [3][4].
AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
DjangoPyPI | >= 2.2a1, < 2.2.26 | 2.2.26 |
DjangoPyPI | >= 3.2a1, < 3.2.11 | 3.2.11 |
DjangoPyPI | >= 4.0a1, < 4.0.1 | 4.0.1 |
Affected products
13- Django/Djangodescription
- osv-coords12 versionspkg:bitnami/djangopkg:pypi/djangopkg:rpm/opensuse/python-Django4&distro=openSUSE%20Tumbleweedpkg:rpm/opensuse/python-Django6&distro=openSUSE%20Tumbleweedpkg:rpm/opensuse/python-Django&distro=openSUSE%20Leap%2015.3pkg:rpm/opensuse/python-Django&distro=openSUSE%20Tumbleweedpkg:rpm/suse/python-Django1&distro=SUSE%20OpenStack%20Cloud%209pkg:rpm/suse/python-Django1&distro=SUSE%20OpenStack%20Cloud%20Crowbar%209pkg:rpm/suse/python-Django&distro=HPE%20Helion%20OpenStack%208pkg:rpm/suse/python-Django&distro=SUSE%20OpenStack%20Cloud%208pkg:rpm/suse/python-Django&distro=SUSE%20OpenStack%20Cloud%20Crowbar%208pkg:rpm/suse/python-Django&distro=SUSE%20Package%20Hub%2015%20SP3
>= 2.2.0, < 2.2.26+ 11 more
- (no CPE)range: >= 2.2.0, < 2.2.26
- (no CPE)range: >= 2.2a1, < 2.2.26
- (no CPE)range: < 4.2.14-1.1
- (no CPE)range: < 6.0-1.1
- (no CPE)range: < 2.2.28-bp153.2.3.1
- (no CPE)range: < 4.0.1-1.1
- (no CPE)range: < 1.11.29-3.30.1
- (no CPE)range: < 1.11.29-3.30.1
- (no CPE)range: < 1.11.29-3.33.1
- (no CPE)range: < 1.11.29-3.33.1
- (no CPE)range: < 1.11.29-3.33.1
- (no CPE)range: < 2.2.28-bp153.2.3.1
Patches
3df79ef03ac86[4.0.x] Fixed CVE-2021-45115 -- Prevented DoS vector in UserAttributeSimilarityValidator.
6 files changed · +92 −15
django/contrib/auth/password_validation.py+38 −2 modified@@ -115,6 +115,36 @@ def get_help_text(self): ) % {'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 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): 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:
docs/releases/2.2.26.txt+13 −1 modified@@ -7,4 +7,16 @@ Django 2.2.26 release notes Django 2.2.26 fixes one security issue with severity "medium" and two security 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>`.
docs/releases/3.2.11.txt+13 −1 modified@@ -7,4 +7,16 @@ Django 3.2.11 release notes Django 3.2.11 fixes one security issue with severity "medium" and two security issues with severity "low" in 3.2.10. -... +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>`.
docs/releases/4.0.1.txt+14 −0 modified@@ -7,6 +7,20 @@ Django 4.0.1 release notes Django 4.0.1 fixes one security issue with severity "medium", two security issues with severity "low", and several bugs in 4.0. +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>`. + Bugfixes ========
docs/topics/auth/passwords.txt+10 −4 modified@@ -609,10 +609,16 @@ Django includes four validators: is used: ``'username', 'first_name', 'last_name', 'email'``. Attributes that don't exist are ignored. - The minimum similarity of a rejected password can be set on a scale of 0 to - 1 with the ``max_similarity`` parameter. A setting of 0 rejects all - passwords, whereas a setting of 1 rejects only passwords that are identical - to an attribute's value. + The maximum allowed similarity of passwords can be set on a scale of 0.1 + to 1.0 with the ``max_similarity`` parameter. This is compared to the + result of :meth:`difflib.SequenceMatcher.quick_ratio`. A value of 0.1 + 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)
tests/auth_tests/test_validators.py+4 −7 modified@@ -150,13 +150,10 @@ def test_validate(self): max_similarity=1, ).validate(user.first_name, user=user) self.assertEqual(cm.exception.messages, [expected_error % "first name"]) - # max_similarity=0 rejects all passwords. - with self.assertRaises(ValidationError) as cm: - UserAttributeSimilarityValidator( - user_attributes=['first_name'], - max_similarity=0, - ).validate('XXX', user=user) - self.assertEqual(cm.exception.messages, [expected_error % "first name"]) + # Very low max_similarity is rejected. + msg = 'max_similarity must be at least 0.1' + with self.assertRaisesMessage(ValueError, msg): + UserAttributeSimilarityValidator(max_similarity=0.09) # Passes validation. self.assertIsNone( UserAttributeSimilarityValidator(user_attributes=['first_name']).validate('testclient', user=user)
2135637fdd5c[2.2.x] Fixed CVE-2021-45115 -- Prevented DoS vector in UserAttributeSimilarityValidator.
4 files changed · +65 −14
django/contrib/auth/password_validation.py+38 −2 modified@@ -115,6 +115,36 @@ def get_help_text(self): ) % {'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 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): 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:
docs/releases/2.2.26.txt+13 −1 modified@@ -7,4 +7,16 @@ Django 2.2.26 release notes Django 2.2.26 fixes one security issue with severity "medium" and two security 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>`.
docs/topics/auth/passwords.txt+10 −4 modified@@ -522,10 +522,16 @@ Django includes four validators: is used: ``'username', 'first_name', 'last_name', 'email'``. Attributes that don't exist are ignored. - The minimum similarity of a rejected password can be set on a scale of 0 to - 1 with the ``max_similarity`` parameter. A setting of 0 rejects all - passwords, whereas a setting of 1 rejects only passwords that are identical - to an attribute's value. + The maximum allowed similarity of passwords can be set on a scale of 0.1 + to 1.0 with the ``max_similarity`` parameter. This is compared to the + result of :meth:`difflib.SequenceMatcher.quick_ratio`. A value of 0.1 + 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)
tests/auth_tests/test_validators.py+4 −7 modified@@ -150,13 +150,10 @@ def test_validate(self): max_similarity=1, ).validate(user.first_name, user=user) self.assertEqual(cm.exception.messages, [expected_error % "first name"]) - # max_similarity=0 rejects all passwords. - with self.assertRaises(ValidationError) as cm: - UserAttributeSimilarityValidator( - user_attributes=['first_name'], - max_similarity=0, - ).validate('XXX', user=user) - self.assertEqual(cm.exception.messages, [expected_error % "first name"]) + # Very low max_similarity is rejected. + msg = 'max_similarity must be at least 0.1' + with self.assertRaisesMessage(ValueError, msg): + UserAttributeSimilarityValidator(max_similarity=0.09) # Passes validation. self.assertIsNone( UserAttributeSimilarityValidator(user_attributes=['first_name']).validate('testclient', user=user)
a8b32fe13bca[3.2.x] Fixed CVE-2021-45115 -- Prevented DoS vector in UserAttributeSimilarityValidator.
5 files changed · +78 −15
django/contrib/auth/password_validation.py+38 −2 modified@@ -115,6 +115,36 @@ def get_help_text(self): ) % {'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 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): 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:
docs/releases/2.2.26.txt+13 −1 modified@@ -7,4 +7,16 @@ Django 2.2.26 release notes Django 2.2.26 fixes one security issue with severity "medium" and two security 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>`.
docs/releases/3.2.11.txt+13 −1 modified@@ -7,4 +7,16 @@ Django 3.2.11 release notes Django 3.2.11 fixes one security issue with severity "medium" and two security issues with severity "low" in 3.2.10. -... +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>`.
docs/topics/auth/passwords.txt+10 −4 modified@@ -539,10 +539,16 @@ Django includes four validators: is used: ``'username', 'first_name', 'last_name', 'email'``. Attributes that don't exist are ignored. - The minimum similarity of a rejected password can be set on a scale of 0 to - 1 with the ``max_similarity`` parameter. A setting of 0 rejects all - passwords, whereas a setting of 1 rejects only passwords that are identical - to an attribute's value. + The maximum allowed similarity of passwords can be set on a scale of 0.1 + to 1.0 with the ``max_similarity`` parameter. This is compared to the + result of :meth:`difflib.SequenceMatcher.quick_ratio`. A value of 0.1 + 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)
tests/auth_tests/test_validators.py+4 −7 modified@@ -150,13 +150,10 @@ def test_validate(self): max_similarity=1, ).validate(user.first_name, user=user) self.assertEqual(cm.exception.messages, [expected_error % "first name"]) - # max_similarity=0 rejects all passwords. - with self.assertRaises(ValidationError) as cm: - UserAttributeSimilarityValidator( - user_attributes=['first_name'], - max_similarity=0, - ).validate('XXX', user=user) - self.assertEqual(cm.exception.messages, [expected_error % "first name"]) + # Very low max_similarity is rejected. + msg = 'max_similarity must be at least 0.1' + with self.assertRaisesMessage(ValueError, msg): + UserAttributeSimilarityValidator(max_similarity=0.09) # Passes validation. self.assertIsNone( UserAttributeSimilarityValidator(user_attributes=['first_name']).validate('testclient', user=user)
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
16- github.com/advisories/GHSA-53qw-q765-4fwwghsaADVISORY
- lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/B4SQG2EAF4WCI2SLRL6XRDJ3RPK3ZRDV/mitrevendor-advisoryx_refsource_FEDORA
- nvd.nist.gov/vuln/detail/CVE-2021-45115ghsaADVISORY
- docs.djangoproject.com/en/4.0/releases/securityghsaWEB
- docs.djangoproject.com/en/4.0/releases/security/mitrex_refsource_MISC
- github.com/django/django/commit/2135637fdd5ce994de110affef9e67dffdf77277ghsaWEB
- github.com/django/django/commit/a8b32fe13bcaed1c0b772fdc53de84abc224fb20ghsaWEB
- github.com/django/django/commit/df79ef03ac867c93caaa6be56bc69e66abfeef8fghsaWEB
- github.com/pypa/advisory-database/tree/main/vulns/django/PYSEC-2022-1.yamlghsaWEB
- groups.google.com/forum/ghsaWEB
- groups.google.com/forum/mitrex_refsource_MISC
- lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/B4SQG2EAF4WCI2SLRL6XRDJ3RPK3ZRDVghsaWEB
- security.netapp.com/advisory/ntap-20220121-0005ghsaWEB
- security.netapp.com/advisory/ntap-20220121-0005/mitrex_refsource_CONFIRM
- www.djangoproject.com/weblog/2022/jan/04/security-releasesghsaWEB
- www.djangoproject.com/weblog/2022/jan/04/security-releases/mitrex_refsource_CONFIRM
News mentions
0No linked articles in our index yet.