Fixed a security issue related to password resets
Full disclosure and new release are forthcoming
This commit is contained in:
parent
3e0857041b
commit
9305c0e12d
|
@ -55,6 +55,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/')),
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ from django.conf import global_settings, 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_text
|
||||
|
@ -103,6 +104,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'})
|
||||
|
|
|
@ -163,7 +163,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:
|
||||
|
|
|
@ -180,6 +180,11 @@ class HttpRequest(object):
|
|||
server_port = str(self.META['SERVER_PORT'])
|
||||
if server_port != ('443' if self.is_secure() else '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…
Reference in New Issue