Author: Luke Faraone <lfaraone@debian.org>
Date: 2013-09-15
Description: Ensure that passwords are never long enough for a DoS.
 * Limit the password length to 4096 bytes
 * Password hashers will raise a ValueError
 * django.contrib.auth forms will fail validation
 * Document in release notes that this is a backwards incompatible change

Thanks to Josh Wright for the report, and Donald Stufft for the patch for
modern Django.

Unfortuantely 1.2.x is no loger supported upstream and the code has changed
substatially in the interim; this is a loose adaptation of the 1.4.x patch.

--- a/django/contrib/auth/forms.py
+++ b/django/contrib/auth/forms.py
@@ -1,4 +1,4 @@
-from django.contrib.auth.models import User
+from django.contrib.auth.models import User, MAXIMUM_PASSWORD_LENGTH
 from django.contrib.auth import authenticate
 from django.contrib.auth.tokens import default_token_generator
 from django.contrib.sites.models import Site
@@ -14,9 +14,9 @@
     username = forms.RegexField(label=_("Username"), max_length=30, regex=r'^[\w.@+-]+$',
         help_text = _("Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only."),
         error_messages = {'invalid': _("This value may contain only letters, numbers and @/./+/-/_ characters.")})
-    password1 = forms.CharField(label=_("Password"), widget=forms.PasswordInput)
+    password1 = forms.CharField(label=_("Password"), widget=forms.PasswordInput, max_length=MAXIMUM_PASSWORD_LENGTH)
     password2 = forms.CharField(label=_("Password confirmation"), widget=forms.PasswordInput,
-        help_text = _("Enter the same password as above, for verification."))
+        max_length=MAXIMUM_PASSWORD_LENGTH, help_text = _("Enter the same password as above, for verification."))
 
     class Meta:
         model = User
@@ -64,7 +64,11 @@
     username/password logins.
     """
     username = forms.CharField(label=_("Username"), max_length=30)
-    password = forms.CharField(label=_("Password"), widget=forms.PasswordInput)
+    password = forms.CharField(
+        label=_("Password"),
+        widget=forms.PasswordInput,
+        max_length=MAXIMUM_PASSWORD_LENGTH,
+    )
 
     def __init__(self, request=None, *args, **kwargs):
         """
@@ -147,8 +151,8 @@
     A form that lets a user change set his/her password without
     entering the old password
     """
-    new_password1 = forms.CharField(label=_("New password"), widget=forms.PasswordInput)
-    new_password2 = forms.CharField(label=_("New password confirmation"), widget=forms.PasswordInput)
+    new_password1 = forms.CharField(label=_("New password"), widget=forms.PasswordInput, max_length=MAXIMUM_PASSWORD_LENGTH)
+    new_password2 = forms.CharField(label=_("New password confirmation"), widget=forms.PasswordInput, max_length=MAXIMUM_PASSWORD_LENGTH)
 
     def __init__(self, user, *args, **kwargs):
         self.user = user
@@ -173,7 +177,7 @@
     A form that lets a user change his/her password by entering
     their old password.
     """
-    old_password = forms.CharField(label=_("Old password"), widget=forms.PasswordInput)
+    old_password = forms.CharField(label=_("Old password"), widget=forms.PasswordInput, max_length=MAXIMUM_PASSWORD_LENGTH)
 
     def clean_old_password(self):
         """
@@ -189,8 +193,8 @@
     """
     A form used to change the password of a user in the admin interface.
     """
-    password1 = forms.CharField(label=_("Password"), widget=forms.PasswordInput)
-    password2 = forms.CharField(label=_("Password (again)"), widget=forms.PasswordInput)
+    password1 = forms.CharField(label=_("Password"), widget=forms.PasswordInput, max_length=MAXIMUM_PASSWORD_LENGTH)
+    password2 = forms.CharField(label=_("Password (again)"), widget=forms.PasswordInput, max_length=MAXIMUM_PASSWORD_LENGTH)
 
     def __init__(self, user, *args, **kwargs):
         self.user = user
--- a/django/contrib/auth/models.py
+++ b/django/contrib/auth/models.py
@@ -10,7 +10,7 @@
 from django.utils.hashcompat import md5_constructor, sha_constructor
 from django.utils.translation import ugettext_lazy as _
 
-
+MAXIMUM_PASSWORD_LENGTH = 4096  # The maximum length a password can be to prevent DoS
 UNUSABLE_PASSWORD = '!' # This will never be a valid hash
 
 def get_hexdigest(algorithm, salt, raw_password):
@@ -18,6 +18,9 @@
     Returns a string of the hexdigest of the given plaintext password and salt
     using the given algorithm ('md5', 'sha1' or 'crypt').
     """
+    if len(raw_password) > MAXIMUM_PASSWORD_LENGTH:
+        raise ValueError("Invalid password; Must be less than or equal"
+                         " to %d bytes" % MAXIMUM_PASSWORD_LENGTH)
     raw_password, salt = smart_str(raw_password), smart_str(salt)
     if algorithm == 'crypt':
         try:
--- a/django/contrib/auth/tests/basic.py
+++ b/django/contrib/auth/tests/basic.py
@@ -14,6 +14,11 @@
 False
 >>> u.has_usable_password()
 False
+>>> u.set_password("a"*4100)
+Traceback (most recent call last):
+    ...
+ValueError: Invalid password; Must be less than or equal to 4096 bytes
+
 >>> u2 = User.objects.create_user('testuser2', 'test2@example.com')
 >>> u2.has_usable_password()
 False
