Skip to content
Permalink
Browse files

[1.8.x] Fixed CVE-2016-9014 -- Validated Host header when DEBUG=True.

This is a security fix.
  • Loading branch information...
timgraham committed Oct 17, 2016
1 parent 70f9995 commit c401ae9a7dfb1a94a8a61927ed541d6f93089587
Showing with 49 additions and 21 deletions.
  1. +5 −4 django/http/request.py
  2. +7 −3 docs/ref/settings.txt
  3. +22 −0 docs/releases/1.8.16.txt
  4. +15 −14 tests/requests/tests.py
@@ -85,12 +85,13 @@ def get_host(self):
if server_port != ('443' if self.is_secure() else '80'):
host = '%s:%s' % (host, server_port)

# There is no hostname validation when DEBUG=True
if settings.DEBUG:
return host
# Allow variants of localhost if ALLOWED_HOSTS is empty and DEBUG=True.
allowed_hosts = settings.ALLOWED_HOSTS
if settings.DEBUG and not allowed_hosts:
allowed_hosts = ['localhost', '127.0.0.1', '[::1]']

domain, port = split_domain_port(host)
if domain and validate_host(domain, settings.ALLOWED_HOSTS):
if domain and validate_host(domain, allowed_hosts):
return host
else:
msg = "Invalid HTTP_HOST header: %r." % host
@@ -108,14 +108,18 @@ If the ``Host`` header (or ``X-Forwarded-Host`` if
list, the :meth:`django.http.HttpRequest.get_host()` method will raise
:exc:`~django.core.exceptions.SuspiciousOperation`.

When :setting:`DEBUG` is ``True`` or when running tests, host validation is
disabled; any host will be accepted. Thus it's usually only necessary to set it
in production.
When :setting:`DEBUG` is ``True`` and ``ALLOWED_HOSTS`` is empty, the host
is validated against ``['localhost', '127.0.0.1', '[::1]']``.

This validation only applies via :meth:`~django.http.HttpRequest.get_host()`;
if your code accesses the ``Host`` header directly from ``request.META`` you
are bypassing this security protection.

.. versionchanged:: 1.8.16

In older versions, ``ALLOWED_HOSTS`` wasn't checked if ``DEBUG=True``, but
it's now checked to prevent a DNS rebinding attack.

.. setting:: ALLOWED_INCLUDE_ROOTS

ALLOWED_INCLUDE_ROOTS
@@ -19,3 +19,25 @@ the ``manage.py test --keepdb`` option or if the user has an active session
(such as an attacker's connection).

A randomly generated password is now used for each test run.

DNS rebinding vulnerability when ``DEBUG=True``
===============================================

Older versions of Django don't validate the ``Host`` header against
``settings.ALLOWED_HOSTS`` when ``settings.DEBUG=True``. This makes them
vulnerable to a `DNS rebinding attack
<http://benmmurphy.github.io/blog/2016/07/11/rails-webconsole-dns-rebinding/>`_.

While Django doesn't ship a module that allows remote code execution, this is
at least a cross-site scripting vector, which could be quite serious if
developers load a copy of the production database in development or connect to
some production services for which there's no development instance, for
example. If a project uses a package like the ``django-debug-toolbar``, then
the attacker could execute arbitrary SQL, which could be especially bad if the
developers connect to the database with a superuser account.

``settings.ALLOWED_HOSTS`` is now validated regardless of ``DEBUG``. For
convenience, if ``ALLOWED_HOSTS`` is empty and ``DEBUG=True``, the following
variations of localhost are allowed ``['localhost', '127.0.0.1', '::1']``. If
your local settings file has your production ``ALLOWED_HOSTS`` value, you must
now omit it to get those fallback values.
@@ -673,21 +673,22 @@ def test_http_get_host_with_x_forwarded_host(self):
request.get_host()

@override_settings(DEBUG=True, ALLOWED_HOSTS=[])
def test_host_validation_disabled_in_debug_mode(self):
"""If ALLOWED_HOSTS is empty and DEBUG is True, all hosts pass."""
request = HttpRequest()
request.META = {
'HTTP_HOST': 'example.com',
}
self.assertEqual(request.get_host(), 'example.com')
def test_host_validation_in_debug_mode(self):
"""
If ALLOWED_HOSTS is empty and DEBUG is True, variants of localhost are
allowed.
"""
valid_hosts = ['localhost', '127.0.0.1', '[::1]']
for host in valid_hosts:
request = HttpRequest()
request.META = {'HTTP_HOST': host}
self.assertEqual(request.get_host(), host)

# Invalid hostnames would normally raise a SuspiciousOperation,
# but we have DEBUG=True, so this check is disabled.
request = HttpRequest()
request.META = {
'HTTP_HOST': "invalid_hostname.com",
}
self.assertEqual(request.get_host(), "invalid_hostname.com")
# Other hostnames raise a SuspiciousOperation.
with self.assertRaises(SuspiciousOperation):
request = HttpRequest()
request.META = {'HTTP_HOST': 'example.com'}
request.get_host()

@override_settings(ALLOWED_HOSTS=[])
def test_get_host_suggestion_of_allowed_host(self):

3 comments on commit c401ae9

@chrisdev

This comment has been minimized.

Copy link

replied Dec 19, 2016

if settings.DEBUG and not allowed_hosts:
       allowed_hosts = ['localhost', '127.0.0.1', '[::1]']

@timgraham I know that this commit was all about the DNS rebinding issue. But this change has some implications for many common development workflows. It means, for example, that I can no longer run ./manage.py runserver 0.0.0.0:8000 to test the mobile version of my site/app on the local network using a mobile device without adding the IP address of the mobile device to my allowed_hosts even though I'm in "development". Hey and If I test again after a few mins I may have to add a different IP to allowed_hosts

@timgraham

This comment has been minimized.

Copy link
Member Author

replied Dec 19, 2016

Hi, commenting on a commit isn't a good place to have a discussion. There's a similar thread on django-developers.

@chrisdev

This comment has been minimized.

Copy link

replied Dec 19, 2016

@timgraham agree! sorry about that.

Please sign in to comment.
You can’t perform that action at this time.