summaryrefslogtreecommitdiff
path: root/django
diff options
context:
space:
mode:
authorCarl Meyer <carl@oddbird.net>2013-02-09 11:32:51 -0700
committerCarl Meyer <carl@oddbird.net>2013-02-19 10:37:54 -0700
commit9936fdb11d0bbf0bd242f259bfb97bbf849d16f8 (patch)
tree43770281aab2cce7da09de070c347235d7d31ebe /django
parent57b62a74cb44ac3cf8b1a9d4cf75bfe953208814 (diff)
[1.4.x] Added ALLOWED_HOSTS setting for HTTP host header validation.
This is a security fix; disclosure and advisory coming shortly.
Diffstat (limited to 'django')
-rw-r--r--django/conf/global_settings.py4
-rw-r--r--django/conf/project_template/project_name/settings.py4
-rw-r--r--django/contrib/auth/tests/views.py1
-rw-r--r--django/contrib/contenttypes/tests.py2
-rw-r--r--django/contrib/sites/tests.py2
-rw-r--r--django/http/__init__.py51
-rw-r--r--django/test/utils.py6
7 files changed, 65 insertions, 5 deletions
diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py
index bd85c121b8..026e39b7c9 100644
--- a/django/conf/global_settings.py
+++ b/django/conf/global_settings.py
@@ -29,6 +29,10 @@ ADMINS = ()
# * Receive x-headers
INTERNAL_IPS = ()
+# Hosts/domain names that are valid for this site.
+# "*" matches anything, ".example.com" matches example.com and all subdomains
+ALLOWED_HOSTS = ['*']
+
# Local time zone for this installation. All choices can be found here:
# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name (although not all
# systems may support all possibilities). When USE_TZ is True, this is
diff --git a/django/conf/project_template/project_name/settings.py b/django/conf/project_template/project_name/settings.py
index 0eccc4eaf5..5780aca012 100644
--- a/django/conf/project_template/project_name/settings.py
+++ b/django/conf/project_template/project_name/settings.py
@@ -20,6 +20,10 @@ DATABASES = {
}
}
+# Hosts/domain names that are valid for this site; required if DEBUG is False
+# See https://docs.djangoproject.com/en/{{ docs_version }}/ref/settings/#allowed-hosts
+ALLOWED_HOSTS = []
+
# Local time zone for this installation. Choices can be found here:
# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
# although not all choices may be available on all operating systems.
diff --git a/django/contrib/auth/tests/views.py b/django/contrib/auth/tests/views.py
index d295bb8c10..603d380e9d 100644
--- a/django/contrib/auth/tests/views.py
+++ b/django/contrib/auth/tests/views.py
@@ -107,6 +107,7 @@ class PasswordResetTest(AuthViewsTestCase):
self.assertEqual(len(mail.outbox), 1)
self.assertEqual("staffmember@example.com", mail.outbox[0].from_email)
+ @override_settings(ALLOWED_HOSTS=['adminsite.com'])
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/',
diff --git a/django/contrib/contenttypes/tests.py b/django/contrib/contenttypes/tests.py
index 3b7906c812..66226a78b8 100644
--- a/django/contrib/contenttypes/tests.py
+++ b/django/contrib/contenttypes/tests.py
@@ -9,6 +9,7 @@ from django.contrib.sites.models import Site
from django.http import HttpRequest, Http404
from django.test import TestCase
from django.utils.encoding import smart_str
+from django.test.utils import override_settings
class FooWithoutUrl(models.Model):
@@ -114,6 +115,7 @@ class ContentTypesTests(TestCase):
FooWithUrl: ContentType.objects.get_for_model(FooWithUrl),
})
+ @override_settings(ALLOWED_HOSTS=['example.com'])
def test_shortcut_view(self):
"""
Check that the shortcut view (used for the admin "view on site"
diff --git a/django/contrib/sites/tests.py b/django/contrib/sites/tests.py
index 828badb386..1fd52e657e 100644
--- a/django/contrib/sites/tests.py
+++ b/django/contrib/sites/tests.py
@@ -3,6 +3,7 @@ from django.contrib.sites.models import Site, RequestSite, get_current_site
from django.core.exceptions import ObjectDoesNotExist
from django.http import HttpRequest
from django.test import TestCase
+from django.test.utils import override_settings
class SitesFrameworkTests(TestCase):
@@ -39,6 +40,7 @@ class SitesFrameworkTests(TestCase):
site = Site.objects.get_current()
self.assertEqual(u"Example site", site.name)
+ @override_settings(ALLOWED_HOSTS=['example.com'])
def test_get_current_site(self):
# Test that the correct Site object is returned
request = HttpRequest()
diff --git a/django/http/__init__.py b/django/http/__init__.py
index da993eb8d3..4f5fbe6cf3 100644
--- a/django/http/__init__.py
+++ b/django/http/__init__.py
@@ -215,11 +215,12 @@ class HttpRequest(object):
if server_port != (self.is_secure() and '443' or '80'):
host = '%s:%s' % (host, server_port)
- # Disallow potentially poisoned hostnames.
- if not host_validation_re.match(host.lower()):
- raise SuspiciousOperation('Invalid HTTP_HOST header: %s' % host)
-
- return host
+ allowed_hosts = ['*'] if settings.DEBUG else settings.ALLOWED_HOSTS
+ if validate_host(host, allowed_hosts):
+ return host
+ else:
+ raise SuspiciousOperation(
+ "Invalid HTTP_HOST header (you may need to set ALLOWED_HOSTS): %s" % host)
def get_full_path(self):
# RFC 3986 requires query string arguments to be in the ASCII range.
@@ -799,3 +800,43 @@ def str_to_unicode(s, encoding):
else:
return s
+def validate_host(host, allowed_hosts):
+ """
+ Validate the given host header value for this site.
+
+ Check that the host looks valid and matches a host or host pattern in the
+ given list of ``allowed_hosts``. Any pattern beginning with a period
+ matches a domain and all its subdomains (e.g. ``.example.com`` matches
+ ``example.com`` and any subdomain), ``*`` matches anything, and anything
+ else must match exactly.
+
+ Return ``True`` for a valid host, ``False`` otherwise.
+
+ """
+ # All validation is case-insensitive
+ host = host.lower()
+
+ # Basic sanity check
+ if not host_validation_re.match(host):
+ return False
+
+ # Validate only the domain part.
+ if host[-1] == ']':
+ # It's an IPv6 address without a port.
+ domain = host
+ else:
+ domain = host.rsplit(':', 1)[0]
+
+ for pattern in allowed_hosts:
+ pattern = pattern.lower()
+ match = (
+ pattern == '*' or
+ pattern.startswith('.') and (
+ domain.endswith(pattern) or domain == pattern[1:]
+ ) or
+ pattern == domain
+ )
+ if match:
+ return True
+
+ return False
diff --git a/django/test/utils.py b/django/test/utils.py
index ed5ab590a7..6d6b6e177f 100644
--- a/django/test/utils.py
+++ b/django/test/utils.py
@@ -75,6 +75,9 @@ def setup_test_environment():
mail.original_email_backend = settings.EMAIL_BACKEND
settings.EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend'
+ settings._original_allowed_hosts = settings.ALLOWED_HOSTS
+ settings.ALLOWED_HOSTS = ['*']
+
mail.outbox = []
deactivate()
@@ -93,6 +96,9 @@ def teardown_test_environment():
settings.EMAIL_BACKEND = mail.original_email_backend
del mail.original_email_backend
+ settings.ALLOWED_HOSTS = settings._original_allowed_hosts
+ del settings._original_allowed_hosts
+
del mail.outbox