Commit b0c56b89 authored by Matt Robenolt's avatar Matt Robenolt Committed by Tim Graham
Browse files

Fixed #24496 -- Added CSRF Referer checking against CSRF_COOKIE_DOMAIN.

Thanks Seth Gottlieb for help with the documentation and
Carl Meyer and Joshua Kehn for reviews.
parent 535809e1
Loading
Loading
Loading
Loading
+2 −9
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@ from django.utils.datastructures import ImmutableList, MultiValueDict
from django.utils.encoding import (
    escape_uri_path, force_bytes, force_str, force_text, iri_to_uri,
)
from django.utils.http import is_same_domain
from django.utils.six.moves.urllib.parse import (
    parse_qsl, quote, urlencode, urljoin, urlsplit,
)
@@ -546,15 +547,7 @@ def validate_host(host, allowed_hosts):
    host = host[:-1] if host.endswith('.') else host

    for pattern in allowed_hosts:
        pattern = pattern.lower()
        match = (
            pattern == '*' or
            pattern.startswith('.') and (
                host.endswith(pattern) or host == pattern[1:]
            ) or
            pattern == host
        )
        if match:
        if pattern == '*' or is_same_domain(host, pattern):
            return True

    return False
+29 −6
Original line number Diff line number Diff line
@@ -14,7 +14,8 @@ from django.core.urlresolvers import get_callable
from django.utils.cache import patch_vary_headers
from django.utils.crypto import constant_time_compare, get_random_string
from django.utils.encoding import force_text
from django.utils.http import same_origin
from django.utils.http import is_same_domain
from django.utils.six.moves.urllib.parse import urlparse

logger = logging.getLogger('django.request')

@@ -22,6 +23,8 @@ REASON_NO_REFERER = "Referer checking failed - no Referer."
REASON_BAD_REFERER = "Referer checking failed - %s does not match any trusted origins."
REASON_NO_CSRF_COOKIE = "CSRF cookie not set."
REASON_BAD_TOKEN = "CSRF token missing or incorrect."
REASON_MALFORMED_REFERER = "Referer checking failed - Referer is malformed."
REASON_INSECURE_REFERER = "Referer checking failed - Referer is insecure while host is secure."

CSRF_KEY_LENGTH = 32

@@ -154,15 +157,35 @@ class CsrfViewMiddleware(object):
                if referer is None:
                    return self._reject(request, REASON_NO_REFERER)

                referer = urlparse(referer)

                # Make sure we have a valid URL for Referer.
                if '' in (referer.scheme, referer.netloc):
                    return self._reject(request, REASON_MALFORMED_REFERER)

                # Ensure that our Referer is also secure.
                if referer.scheme != 'https':
                    return self._reject(request, REASON_INSECURE_REFERER)

                # If there isn't a CSRF_COOKIE_DOMAIN, assume we need an exact
                # match on host:port. If not, obey the cookie rules.
                if settings.CSRF_COOKIE_DOMAIN is None:
                    # request.get_host() includes the port.
                    good_referer = request.get_host()
                else:
                    good_referer = settings.CSRF_COOKIE_DOMAIN
                    server_port = request.META['SERVER_PORT']
                    if server_port not in ('443', '80'):
                        good_referer = '%s:%s' % (good_referer, server_port)

                # Here we generate a list of all acceptable HTTP referers,
                # including the current host since that has been validated
                # upstream.
                good_hosts = list(settings.CSRF_TRUSTED_ORIGINS)
                # Note that request.get_host() includes the port.
                good_hosts.append(request.get_host())
                good_referers = ['https://{0}/'.format(host) for host in good_hosts]
                if not any(same_origin(referer, host) for host in good_referers):
                    reason = REASON_BAD_REFERER % referer
                good_hosts.append(good_referer)

                if not any(is_same_domain(referer.netloc, host) for host in good_hosts):
                    reason = REASON_BAD_REFERER % referer.geturl()
                    return self._reject(request, reason)

            if csrf_token is None:
+14 −8
Original line number Diff line number Diff line
@@ -253,18 +253,24 @@ def quote_etag(etag):
    return '"%s"' % etag.replace('\\', '\\\\').replace('"', '\\"')


def same_origin(url1, url2):
def is_same_domain(host, pattern):
    """
    Checks if two URLs are 'same-origin'
    Return ``True`` if the host is either an exact match or a match
    to the wildcard pattern.

    Any pattern beginning with a period matches a domain and all of its
    subdomains. (e.g. ``.example.com`` matches ``example.com`` and
    ``foo.example.com``). Anything else is an exact string match.
    """
    p1, p2 = urlparse(url1), urlparse(url2)
    try:
        o1 = (p1.scheme, p1.hostname, p1.port or PROTOCOL_TO_PORT[p1.scheme])
        o2 = (p2.scheme, p2.hostname, p2.port or PROTOCOL_TO_PORT[p2.scheme])
        return o1 == o2
    except (ValueError, KeyError):
    if not pattern:
        return False

    pattern = pattern.lower()
    return (
        pattern[0] == '.' and (host.endswith(pattern) or host == pattern[1:]) or
        pattern == host
    )


def is_safe_url(url, host=None):
    """
+16 −4
Original line number Diff line number Diff line
@@ -257,11 +257,19 @@ The CSRF protection is based on the following things:
   due to the fact that HTTP 'Set-Cookie' headers are (unfortunately) accepted
   by clients that are talking to a site under HTTPS.  (Referer checking is not
   done for HTTP requests because the presence of the Referer header is not
   reliable enough under HTTP.) Expanding the accepted referers beyond the
   current host can be done with the :setting:`CSRF_TRUSTED_ORIGINS` setting.
   reliable enough under HTTP.)

This ensures that only forms that have originated from your Web site can be used
to POST data back.
   If the :setting:`CSRF_COOKIE_DOMAIN` setting is set, the referer is compared
   against it. This setting supports subdomains. For example,
   ``CSRF_COOKIE_DOMAIN = '.example.com'`` will allow POST requests from
   ``www.example.com`` and ``api.example.com``. If the setting is not set, then
   the referer must match the HTTP ``Host`` header.

   Expanding the accepted referers beyond the current host or cookie domain can
   be done with the :setting:`CSRF_TRUSTED_ORIGINS` setting.

This ensures that only forms that have originated from trusted domains can be
used to POST data back.

It deliberately ignores GET requests (and other requests that are defined as
'safe' by :rfc:`2616`). These requests ought never to have any potentially
@@ -269,6 +277,10 @@ dangerous side effects , and so a CSRF attack with a GET request ought to be
harmless. :rfc:`2616` defines POST, PUT and DELETE as 'unsafe', and all other
methods are assumed to be unsafe, for maximum protection.

.. versionchanged:: 1.9

    Checking against the :setting:`CSRF_COOKIE_DOMAIN` setting was added.

Caching
=======

+2 −0
Original line number Diff line number Diff line
@@ -444,6 +444,8 @@ header that matches the origin present in the ``Host`` header. This prevents,
for example, a ``POST`` request from ``subdomain.example.com`` from succeeding
against ``api.example.com``. If you need cross-origin unsafe requests over
HTTPS, continuing the example, add ``"subdomain.example.com"`` to this list.
The setting also supports subdomains, so you could add ``".example.com"``, for
example, to allow access from all subdomains of ``example.com``.

.. setting:: DATABASES

Loading