Commit 27cd872e authored by Carl Meyer's avatar Carl Meyer Committed by Aymeric Augustin
Browse files

[1.3.x] Added ALLOWED_HOSTS setting for HTTP host header validation.

This is a security fix; disclosure and advisory coming shortly.
parent 6e70f674
Loading
Loading
Loading
Loading
+4 −0
Original line number Diff line number Diff line
@@ -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).
+4 −0
Original line number Diff line number Diff line
@@ -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.
+49 −5
Original line number Diff line number Diff line
@@ -168,11 +168,15 @@ 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)

        if settings.DEBUG:
            allowed_hosts = ['*']
        else:
            allowed_hosts = 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.
@@ -704,3 +708,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
+6 −0
Original line number Diff line number Diff line
@@ -76,6 +76,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()
@@ -97,6 +100,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


+36 −0
Original line number Diff line number Diff line
@@ -82,6 +82,42 @@ of (Full name, e-mail address). Example::
Note that Django will e-mail *all* of these people whenever an error happens.
See :doc:`/howto/error-reporting` for more information.

.. setting:: ALLOWED_HOSTS

ALLOWED_HOSTS
-------------

Default: ``['*']``

A list of strings representing the host/domain names that this Django site can
serve. This is a security measure to prevent an attacker from poisoning caches
and password reset emails with links to malicious hosts by submitting requests
with a fake HTTP ``Host`` header, which is possible even under many
seemingly-safe webserver configurations.

Values in this list can be fully qualified names (e.g. ``'www.example.com'``),
in which case they will be matched against the request's ``Host`` header
exactly (case-insensitive, not including port). A value beginning with a period
can be used as a subdomain wildcard: ``'.example.com'`` will match
``example.com``, ``www.example.com``, and any other subdomain of
``example.com``. A value of ``'*'`` will match anything; in this case you are
responsible to provide your own validation of the ``Host`` header (perhaps in a
middleware; if so this middleware must be listed first in
:setting:`MIDDLEWARE_CLASSES`).

If the ``Host`` header (or ``X-Forwarded-Host`` if
:setting:`USE_X_FORWARDED_HOST` is enabled) does not match any value in this
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.

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.

.. setting:: ALLOWED_INCLUDE_ROOTS

ALLOWED_INCLUDE_ROOTS
Loading