diff -Nru python-django-1.3.1/debian/changelog python-django-1.3.1/debian/changelog --- python-django-1.3.1/debian/changelog 2012-09-06 08:57:38.000000000 -0400 +++ python-django-1.3.1/debian/changelog 2012-10-23 17:23:04.000000000 -0400 @@ -1,3 +1,10 @@ +python-django (1.3.1-4ubuntu1.3) precise-security; urgency=low + + * Security update to the 1.3.x branch to fix HOST header poisoning + (LP: #1068486) + + -- Mackenzie Morgan Tue, 23 Oct 2012 17:22:26 -0400 + python-django (1.3.1-4ubuntu1.2) precise-security; urgency=high [ Scott Kitterman ] diff -Nru python-django-1.3.1/debian/patches/10_host_head_poisoning_fix.diff python-django-1.3.1/debian/patches/10_host_head_poisoning_fix.diff --- python-django-1.3.1/debian/patches/10_host_head_poisoning_fix.diff 1969-12-31 19:00:00.000000000 -0500 +++ python-django-1.3.1/debian/patches/10_host_head_poisoning_fix.diff 2012-10-23 17:21:27.000000000 -0400 @@ -0,0 +1,88 @@ +--- a/django/contrib/auth/urls.py ++++ b/django/contrib/auth/urls.py +@@ -11,6 +11,7 @@ + (r'^password_change/done/$', 'django.contrib.auth.views.password_change_done'), + (r'^password_reset/$', 'django.contrib.auth.views.password_reset'), + (r'^password_reset/done/$', 'django.contrib.auth.views.password_reset_done'), ++ (r'^admin_password_reset/$', 'django.contrib.auth.views.password_reset', dict(is_admin_site=True)), + (r'^reset/(?P[0-9A-Za-z]{1,13})-(?P[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$', 'django.contrib.auth.views.password_reset_confirm'), + (r'^reset/done/$', 'django.contrib.auth.views.password_reset_complete'), + ) +--- a/django/contrib/auth/tests/views.py ++++ b/django/contrib/auth/tests/views.py +@@ -9,6 +9,7 @@ + from django.contrib.auth.models import User + from django.test import TestCase + from django.core import mail ++from django.core.exceptions import SuspiciousOperation + from django.core.urlresolvers import reverse + from django.http import QueryDict + +@@ -69,6 +70,44 @@ + self.assertEqual(len(mail.outbox), 1) + self.assertEqual("staffmember@example.com", mail.outbox[0].from_email) + ++ def test_admin_reset(self): ++ "If the reset view is marked as being for admin, the HTTP_HOST header is used for a domain override." ++ response = self.client.post('/admin_password_reset/', ++ {'email': 'staffmember@example.com'}, ++ HTTP_HOST='adminsite.com' ++ ) ++ self.assertEqual(response.status_code, 302) ++ self.assertEqual(len(mail.outbox), 1) ++ self.assertTrue("http://adminsite.com" in mail.outbox[0].body) ++ self.assertEqual(settings.DEFAULT_FROM_EMAIL, mail.outbox[0].from_email) ++ ++ def test_poisoned_http_host(self): ++ "Poisoned HTTP_HOST headers can't be used for reset emails" ++ # This attack is based on the way browsers handle URLs. The colon ++ # should be used to separate the port, but if the URL contains an @, ++ # the colon is interpreted as part of a username for login purposes, ++ # making 'evil.com' the request domain. Since HTTP_HOST is used to ++ # produce a meaningful reset URL, we need to be certain that the ++ # HTTP_HOST header isn't poisoned. This is done as a check when get_host() ++ # is invoked, but we check here as a practical consequence. ++ def test_host_poisoning(): ++ self.client.post('/password_reset/', ++ {'email': 'staffmember@example.com'}, ++ HTTP_HOST='www.example:dr.frankenstein@evil.tld' ++ ) ++ self.assertRaises(SuspiciousOperation, test_host_poisoning) ++ self.assertEqual(len(mail.outbox), 0) ++ ++ def test_poisoned_http_host_admin_site(self): ++ "Poisoned HTTP_HOST headers can't be used for reset emails on admin views" ++ def test_host_poisoning(): ++ self.client.post('/admin_password_reset/', ++ {'email': 'staffmember@example.com'}, ++ HTTP_HOST='www.example:dr.frankenstein@evil.tld' ++ ) ++ self.assertRaises(SuspiciousOperation, test_host_poisoning) ++ self.assertEqual(len(mail.outbox), 0) ++ + def _test_confirm_start(self): + # Start by creating the email + response = self.client.post('/password_reset/', {'email': 'staffmember@example.com'}) +--- a/django/contrib/auth/views.py ++++ b/django/contrib/auth/views.py +@@ -151,7 +151,7 @@ + 'request': request, + } + if is_admin_site: +- opts = dict(opts, domain_override=request.META['HTTP_HOST']) ++ opts = dict(opts, domain_override=request.get_host()) + form.save(**opts) + return HttpResponseRedirect(post_reset_redirect) + else: +--- a/django/http/__init__.py ++++ b/django/http/__init__.py +@@ -165,6 +165,9 @@ + server_port = str(self.META['SERVER_PORT']) + if server_port != (self.is_secure() and '443' or '80'): + host = '%s:%s' % (host, server_port) ++ # Disallow potentially poisoned hostnames. ++ if set(';/?@&=+$,').intersection(host): ++ raise SuspiciousOperation('Invalid HTTP_HOST header: %s' % host) + return host + + def get_full_path(self): diff -Nru python-django-1.3.1/debian/patches/series python-django-1.3.1/debian/patches/series --- python-django-1.3.1/debian/patches/series 2012-08-15 03:23:37.000000000 -0400 +++ python-django-1.3.1/debian/patches/series 2012-10-23 17:10:57.000000000 -0400 @@ -12,3 +12,4 @@ security_http_redirects security_image_uploading_two security_image_uploading +10_host_head_poisoning_fix.diff