diff -u python-django-1.1.1/debian/changelog python-django-1.1.1/debian/changelog --- python-django-1.1.1/debian/changelog +++ python-django-1.1.1/debian/changelog @@ -1,3 +1,24 @@ +python-django (1.1.1-2ubuntu1.9) lucid-security; urgency=low + + * SECURITY UPDATE: denial of service via long passwords (LP: #1225784) + - debian/patches/CVE-2013-1443.patch: enforce a maximum password length + in django/contrib/auth/forms.py, django/contrib/auth/models.py, + django/contrib/auth/tests/basic.py. + - CVE-2013-1443 + * SECURITY UPDATE: directory traversal with ssi template tag + - debian/patches/CVE-2013-4315.patch: properly check absolute path in + django/template/defaulttags.py, + tests/regressiontests/templates/tests.py, + tests/regressiontests/templates/templates/*. + - CVE-2013-4315 + * SECURITY UPDATE: possible XSS via is_safe_url + - debian/patches/security-is_safe_url.patch: properly reject URLs which + specify a scheme other then HTTP or HTTPS. + - https://www.djangoproject.com/weblog/2013/aug/13/security-releases-issued/ + - No CVE number + + -- Marc Deslauriers Fri, 20 Sep 2013 09:33:23 -0400 + python-django (1.1.1-2ubuntu1.8) lucid-security; urgency=low * SECURITY UPDATE: host header poisoning (LP: #1089337) diff -u python-django-1.1.1/debian/patches/series python-django-1.1.1/debian/patches/series --- python-django-1.1.1/debian/patches/series +++ python-django-1.1.1/debian/patches/series @@ -26,0 +27,3 @@ +security-is_safe_url.patch +CVE-2013-1443.patch +CVE-2013-4315.patch only in patch2: unchanged: --- python-django-1.1.1.orig/debian/patches/CVE-2013-4315.patch +++ python-django-1.1.1/debian/patches/CVE-2013-4315.patch @@ -0,0 +1,83 @@ +Description: fix directory traversal with ssi template tag +Origin: backport, https://github.com/django/django/commit/87d2750b39f6f2d54b7047225521a44dcd37e896 +Origin: backport, https://github.com/django/django/commit/3203f684e8e51cbfa1b39d7b6a56e340981ad4d5 +Bug-Debian: http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=722605 + +Index: python-django-1.1.1/django/template/defaulttags.py +=================================================================== +--- python-django-1.1.1.orig/django/template/defaulttags.py 2013-09-20 10:52:53.104018424 -0400 ++++ python-django-1.1.1/django/template/defaulttags.py 2013-09-20 10:52:53.096018424 -0400 +@@ -1,5 +1,6 @@ + """Default tags used by the template system, available to all templates.""" + ++import os + import sys + import re + from itertools import cycle as itertools_cycle +@@ -277,6 +278,7 @@ + return '' + + def include_is_allowed(filepath): ++ filepath = os.path.abspath(filepath) + for root in settings.ALLOWED_INCLUDE_ROOTS: + if filepath.startswith(root): + return True +Index: python-django-1.1.1/tests/regressiontests/templates/tests.py +=================================================================== +--- python-django-1.1.1.orig/tests/regressiontests/templates/tests.py 2013-09-20 10:52:53.104018424 -0400 ++++ python-django-1.1.1/tests/regressiontests/templates/tests.py 2013-09-20 10:54:24.476015910 -0400 +@@ -1047,5 +1047,36 @@ + 'autoescape-filtertag01': ("{{ first }}{% filter safe %}{{ first }} x"}, template.TemplateSyntaxError), + } + ++class SSITests(unittest.TestCase): ++ def setUp(self): ++ self.this_dir = os.path.dirname(os.path.abspath(__file__)) ++ self.ssi_dir = os.path.join(self.this_dir, "templates", "first") ++ ++ def render_ssi(self, path): ++ from django.template import Context ++ # the path must exist for the test to be reliable ++ self.assertTrue(os.path.exists(path)) ++ return template.Template('{%% ssi %s %%}' % path).render(Context()) ++ ++ def test_allowed_paths(self): ++ acceptable_path = os.path.join(self.ssi_dir, "..", "first", "test.html") ++ settings.ALLOWED_INCLUDE_ROOTS=(self.ssi_dir,) ++ self.assertEqual(self.render_ssi(acceptable_path), 'First template\n') ++ ++ def test_relative_include_exploit(self): ++ """ ++ May not bypass ALLOWED_INCLUDE_ROOTS with relative paths ++ ++ e.g. if ALLOWED_INCLUDE_ROOTS = ("/var/www",), it should not be ++ possible to do {% ssi "/var/www/../../etc/passwd" %} ++ """ ++ disallowed_paths = [ ++ os.path.join(self.ssi_dir, "..", "ssi_include.html"), ++ os.path.join(self.ssi_dir, "..", "second", "test.html"), ++ ] ++ settings.ALLOWED_INCLUDE_ROOTS=(self.ssi_dir,) ++ for path in disallowed_paths: ++ self.assertEqual(self.render_ssi(path), '') ++ + if __name__ == "__main__": + unittest.main() +Index: python-django-1.1.1/tests/regressiontests/templates/templates/ssi_include.html +=================================================================== +--- /dev/null 1970-01-01 00:00:00.000000000 +0000 ++++ python-django-1.1.1/tests/regressiontests/templates/templates/ssi_include.html 2013-09-20 10:52:53.096018424 -0400 +@@ -0,0 +1 @@ ++This is for testing an ssi include. {{ test }} +Index: python-django-1.1.1/tests/regressiontests/templates/templates/first/test.html +=================================================================== +--- /dev/null 1970-01-01 00:00:00.000000000 +0000 ++++ python-django-1.1.1/tests/regressiontests/templates/templates/first/test.html 2013-09-20 10:52:53.096018424 -0400 +@@ -0,0 +1 @@ ++First template +Index: python-django-1.1.1/tests/regressiontests/templates/templates/second/test.html +=================================================================== +--- /dev/null 1970-01-01 00:00:00.000000000 +0000 ++++ python-django-1.1.1/tests/regressiontests/templates/templates/second/test.html 2013-09-20 10:52:53.096018424 -0400 +@@ -0,0 +1 @@ ++Second template only in patch2: unchanged: --- python-django-1.1.1.orig/debian/patches/CVE-2013-1443.patch +++ python-django-1.1.1/debian/patches/CVE-2013-1443.patch @@ -0,0 +1,109 @@ +Description: fix denial of service via long passwords +Origin: based on password-dos.diff by Luke Faraone +Bug-Debian: http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=723043 +Bug-Ubuntu: https://bugs.launchpad.net/ubuntu/+source/python-django/+bug/1225784 + +Index: python-django-1.1.1/django/contrib/auth/forms.py +=================================================================== +--- python-django-1.1.1.orig/django/contrib/auth/forms.py 2013-09-20 09:27:39.732159139 -0400 ++++ python-django-1.1.1/django/contrib/auth/forms.py 2013-09-20 09:29:13.336156563 -0400 +@@ -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,8 +14,9 @@ + username = forms.RegexField(label=_("Username"), max_length=30, regex=r'^\w+$', + help_text = _("Required. 30 characters or fewer. Alphanumeric characters only (letters, digits and underscores)."), + error_message = _("This value must contain only letters, numbers and underscores.")) +- password1 = forms.CharField(label=_("Password"), widget=forms.PasswordInput) +- password2 = forms.CharField(label=_("Password confirmation"), 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, ++ max_length=MAXIMUM_PASSWORD_LENGTH) + + class Meta: + model = User +@@ -57,7 +58,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): + """ +@@ -140,8 +145,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 +@@ -166,7 +171,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): + """ +@@ -182,8 +187,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 +Index: python-django-1.1.1/django/contrib/auth/models.py +=================================================================== +--- python-django-1.1.1.orig/django/contrib/auth/models.py 2013-09-20 09:27:39.732159139 -0400 ++++ python-django-1.1.1/django/contrib/auth/models.py 2013-09-20 09:29:30.960156078 -0400 +@@ -10,6 +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 + + try: +@@ -22,6 +23,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: +Index: python-django-1.1.1/django/contrib/auth/tests/basic.py +=================================================================== +--- python-django-1.1.1.orig/django/contrib/auth/tests/basic.py 2013-09-20 09:27:39.732159139 -0400 ++++ python-django-1.1.1/django/contrib/auth/tests/basic.py 2013-09-20 09:30:16.824154816 -0400 +@@ -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 only in patch2: unchanged: --- python-django-1.1.1.orig/debian/patches/security-is_safe_url.patch +++ python-django-1.1.1/debian/patches/security-is_safe_url.patch @@ -0,0 +1,34 @@ +Backport of: + +From ec67af0bd609c412b76eaa4cc89968a2a8e5ad6a Mon Sep 17 00:00:00 2001 +From: Jacob Kaplan-Moss +Date: Tue, 13 Aug 2013 11:00:13 -0500 +Subject: [PATCH] Fixed is_safe_url() to reject URLs that use a scheme other + than HTTP/S. + +This is a security fix; disclosure to follow shortly. +--- + django/contrib/auth/tests/views.py | 8 ++++++-- + django/utils/http.py | 7 ++++--- + 2 files changed, 10 insertions(+), 5 deletions(-) + +Index: python-django-1.1.1/django/utils/http.py +=================================================================== +--- python-django-1.1.1.orig/django/utils/http.py 2013-09-20 09:25:21.996162930 -0400 ++++ python-django-1.1.1/django/utils/http.py 2013-09-20 09:25:21.992162930 -0400 +@@ -126,11 +126,12 @@ + def is_safe_url(url, host=None): + """ + Return ``True`` if the url is a safe redirection (i.e. it doesn't point to +- a different host). ++ a different host and uses a safe scheme). + + Always returns ``False`` on an empty url. + """ + if not url: + return False +- netloc = urlparse.urlparse(url)[1] +- return not netloc or netloc == host ++ url_info = urlparse.urlparse(url) ++ return (not url_info[1] or url_info[1] == host) and \ ++ (not url_info[0] or url_info[0] in ['http', 'https'])