Loading django/contrib/auth/tests/urls.py +1 −0 Original line number Diff line number Diff line Loading @@ -51,6 +51,7 @@ urlpatterns = urlpatterns + patterns('', (r'^logout/next_page/$', 'django.contrib.auth.views.logout', dict(next_page='/somewhere/')), (r'^remote_user/$', remote_user_auth_view), (r'^password_reset_from_email/$', 'django.contrib.auth.views.password_reset', dict(from_email='staffmember@example.com')), (r'^admin_password_reset/$', 'django.contrib.auth.views.password_reset', dict(is_admin_site=True)), (r'^login_required/$', login_required(password_reset)), (r'^login_required_login_url/$', login_required(password_reset, login_url='/somewhere/')), Loading django/contrib/auth/tests/views.py +37 −0 Original line number Diff line number Diff line Loading @@ -7,6 +7,7 @@ from django.conf import settings from django.contrib.sites.models import Site, RequestSite from django.contrib.auth.models import User from django.core import mail from django.core.exceptions import SuspiciousOperation from django.core.urlresolvers import reverse, NoReverseMatch from django.http import QueryDict from django.utils.encoding import force_unicode Loading Loading @@ -106,6 +107,42 @@ class PasswordResetTest(AuthViewsTestCase): 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. with self.assertRaises(SuspiciousOperation): self.client.post('/password_reset/', {'email': 'staffmember@example.com'}, HTTP_HOST='www.example:dr.frankenstein@evil.tld' ) 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" with self.assertRaises(SuspiciousOperation): self.client.post('/admin_password_reset/', {'email': 'staffmember@example.com'}, HTTP_HOST='www.example:dr.frankenstein@evil.tld' ) 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'}) Loading django/contrib/auth/views.py +1 −1 Original line number Diff line number Diff line Loading @@ -156,7 +156,7 @@ def password_reset(request, is_admin_site=False, '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: Loading django/http/__init__.py +5 −0 Original line number Diff line number Diff line Loading @@ -212,6 +212,11 @@ class HttpRequest(object): 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): Loading Loading
django/contrib/auth/tests/urls.py +1 −0 Original line number Diff line number Diff line Loading @@ -51,6 +51,7 @@ urlpatterns = urlpatterns + patterns('', (r'^logout/next_page/$', 'django.contrib.auth.views.logout', dict(next_page='/somewhere/')), (r'^remote_user/$', remote_user_auth_view), (r'^password_reset_from_email/$', 'django.contrib.auth.views.password_reset', dict(from_email='staffmember@example.com')), (r'^admin_password_reset/$', 'django.contrib.auth.views.password_reset', dict(is_admin_site=True)), (r'^login_required/$', login_required(password_reset)), (r'^login_required_login_url/$', login_required(password_reset, login_url='/somewhere/')), Loading
django/contrib/auth/tests/views.py +37 −0 Original line number Diff line number Diff line Loading @@ -7,6 +7,7 @@ from django.conf import settings from django.contrib.sites.models import Site, RequestSite from django.contrib.auth.models import User from django.core import mail from django.core.exceptions import SuspiciousOperation from django.core.urlresolvers import reverse, NoReverseMatch from django.http import QueryDict from django.utils.encoding import force_unicode Loading Loading @@ -106,6 +107,42 @@ class PasswordResetTest(AuthViewsTestCase): 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. with self.assertRaises(SuspiciousOperation): self.client.post('/password_reset/', {'email': 'staffmember@example.com'}, HTTP_HOST='www.example:dr.frankenstein@evil.tld' ) 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" with self.assertRaises(SuspiciousOperation): self.client.post('/admin_password_reset/', {'email': 'staffmember@example.com'}, HTTP_HOST='www.example:dr.frankenstein@evil.tld' ) 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'}) Loading
django/contrib/auth/views.py +1 −1 Original line number Diff line number Diff line Loading @@ -156,7 +156,7 @@ def password_reset(request, is_admin_site=False, '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: Loading
django/http/__init__.py +5 −0 Original line number Diff line number Diff line Loading @@ -212,6 +212,11 @@ class HttpRequest(object): 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): Loading