From 22f0f275f07abaa0070a2a8999e6fff5edae271c Mon Sep 17 00:00:00 2001 From: Florian Apolloner Date: Sun, 14 Jun 2020 11:22:22 +0200 Subject: [PATCH] Moved host header parsing into a function that optionally performs validation too. --- django/http/request.py | 86 ++++++++++++++++++++------------------- tests/csrf_tests/tests.py | 24 +++-------- tests/requests/tests.py | 11 +++-- 3 files changed, 58 insertions(+), 63 deletions(-) diff --git a/django/http/request.py b/django/http/request.py index 62ed1d58f0..f5aba3590f 100644 --- a/django/http/request.py +++ b/django/http/request.py @@ -1,6 +1,7 @@ import codecs import copy -import operator +import warnings +from collections import namedtuple from io import BytesIO from itertools import chain from urllib.parse import parse_qsl, quote, urlencode, urljoin, urlsplit @@ -31,7 +32,9 @@ from django.utils.http import is_same_domain, parse_header_parameters from django.utils.regex_helper import _lazy_re_compile RAISE_ERROR = object() -host_validation_re = _lazy_re_compile(r"^([a-z0-9.-]+|\[[a-f0-9]*:[a-f0-9\.:]+\])(?::(\d+))?$") +host_validation_re = _lazy_re_compile(r"^([a-z0-9.-]+|\[[a-f0-9]*:[a-f0-9\.:]+\])(?::([0-9]+))?$") + +ParsedHostHeader = namedtuple('ParsedHostHeader', ['domain', 'port', 'combined']) class UnreadablePostError(OSError): @@ -143,60 +146,62 @@ class HttpRequest: else: self.encoding = self.content_params["charset"] - @cached_property - def _parsed_host_header(self): - use_x_fw_host = settings.USE_X_FORWARDED_HOST - use_x_fw_port = settings.USE_X_FORWARDED_PORT + def _get_parsed_host_header(self, validate=True): + if not hasattr(self, '_parsed_host_obj'): + use_x_fw_host = settings.USE_X_FORWARDED_HOST + use_x_fw_port = settings.USE_X_FORWARDED_PORT - port_in_x_fw_host = False - default_port = ('443' if self.is_secure() else '80') + port_in_x_fw_host = False + default_port = ('443' if self.is_secure() else '80') - if use_x_fw_host and 'HTTP_X_FORWARDED_HOST' in self.META: - host, port = _parse_host_header(self.META['HTTP_X_FORWARDED_HOST']) - port_in_x_fw_host = port != '' - elif 'HTTP_HOST' in self.META: - host, port = _parse_host_header(self.META['HTTP_HOST']) + if use_x_fw_host and 'HTTP_X_FORWARDED_HOST' in self.META: + host, port = _parse_host_header(self.META['HTTP_X_FORWARDED_HOST']) + port_in_x_fw_host = port != '' + elif 'HTTP_HOST' in self.META: + host, port = _parse_host_header(self.META['HTTP_HOST']) + else: + # Reconstruct the host using the algorithm from PEP 333. + host, port = self.META['SERVER_NAME'], str(self.META['SERVER_PORT']) + if port == default_port: + port = '' + + if use_x_fw_port and 'HTTP_X_FORWARDED_PORT' in self.META: + if port_in_x_fw_host: + raise ImproperlyConfigured('HTTP_X_FORWARDED_HOST contains a port number ' + 'and USE_X_FORWARDED_PORT is set to True') + port = self.META['HTTP_X_FORWARDED_PORT'] + + reconstructed = '%s:%s' % (host, port) if port else host + + domain, port = split_domain_port(reconstructed) + parsed_host = self._parsed_host_obj = ParsedHostHeader(domain, port or default_port, reconstructed) else: - # Reconstruct the host using the algorithm from PEP 333. - host, port = self.META['SERVER_NAME'], str(self.META['SERVER_PORT']) - if port == default_port: - port = '' - - if use_x_fw_port and 'HTTP_X_FORWARDED_PORT' in self.META: - if port_in_x_fw_host: - raise ImproperlyConfigured('HTTP_X_FORWARDED_HOST contains a port number ' - 'and USE_X_FORWARDED_PORT is set to True') - port = self.META['HTTP_X_FORWARDED_PORT'] - - reconstructed = '%s:%s' % (host, port) if port else host - return host, port or default_port, reconstructed - - def get_host(self): - """Return the HTTP host using the environment or request headers.""" - _, _, host_header = self._parsed_host_header + parsed_host = self._parsed_host_obj # 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_header) - if domain and validate_host(domain, allowed_hosts): - return host_header - else: - msg = "Invalid HTTP_HOST header: %r." % host_header - if domain: - msg += " You may need to add %r to ALLOWED_HOSTS." % domain + msg = "Invalid HTTP_HOST header: %r." % parsed_host.combined + if validate and not (parsed_host.domain and validate_host(parsed_host.domain, allowed_hosts)): + if parsed_host.domain: + msg += " You may need to add %r to ALLOWED_HOSTS." % parsed_host.domain else: msg += ( " The domain name provided is not valid according to RFC 1034/1035." ) raise DisallowedHost(msg) + return parsed_host + + def get_host(self): + """Return the HTTP host using the environment or request headers.""" + return self._get_parsed_host_header().combined + def get_port(self): """Return the port number for the request as a string.""" - _, port, _ = self._parsed_host_header - return port + return self._get_parsed_host_header().port def get_full_path(self, force_append_slash=False): return self._get_full_path(self.path, force_append_slash) @@ -246,10 +251,9 @@ class HttpRequest: Return an absolute URI from variables available in this request. Skip allowed hosts protection, so may return insecure URI. """ - _, _, host_header = self._parsed_host_header return '{scheme}://{host}{path}'.format( scheme=self.scheme, - host=host_header, + host=self._get_parsed_host_header(validate=False).combined, path=self.get_full_path(), ) diff --git a/tests/csrf_tests/tests.py b/tests/csrf_tests/tests.py index b2a48c2e65..14f0a549e5 100644 --- a/tests/csrf_tests/tests.py +++ b/tests/csrf_tests/tests.py @@ -1274,28 +1274,15 @@ class CsrfViewMiddlewareTests(CsrfViewMiddlewareTestMixin, SimpleTestCase): self.assertEqual(csrf_cookie, TEST_SECRET) self._check_token_present(resp, csrf_cookie) - def test_bare_secret_accepted_and_not_replaced(self): - """ - The csrf cookie is left unchanged if originally not masked. - """ - req = self._get_POST_request_with_token(cookie=TEST_SECRET) - mw = CsrfViewMiddleware(token_view) - mw.process_request(req) - resp = mw.process_view(req, token_view, (), {}) - self.assertIsNone(resp) - resp = mw(req) - csrf_cookie = self._read_csrf_cookie(req, resp) - self.assertEqual(csrf_cookie, TEST_SECRET) - self._check_token_present(resp, csrf_cookie) - @override_settings( - ALLOWED_HOSTS=["www.example.com"], - CSRF_COOKIE_DOMAIN=".example.com", + ALLOWED_HOSTS=['www.example.com'], + CSRF_COOKIE_DOMAIN='.example.com', USE_X_FORWARDED_PORT=True, + USE_X_FORWARDED_HOST=True ) def test_https_good_referer_behind_proxy(self): """ - A POST HTTPS request is accepted when USE_X_FORWARDED_PORT=True. + A POST HTTPS request is accepted when USE_X_FORWARDED_*=True. """ self._test_https_good_referer_behind_proxy() @@ -1428,11 +1415,12 @@ class CsrfViewMiddlewareUseSessionsTests(CsrfViewMiddlewareTestMixin, SimpleTest ALLOWED_HOSTS=["www.example.com"], SESSION_COOKIE_DOMAIN=".example.com", USE_X_FORWARDED_PORT=True, + USE_X_FORWARDED_HOST=True, DEBUG=True, ) def test_https_good_referer_behind_proxy(self): """ - A POST HTTPS request is accepted when USE_X_FORWARDED_PORT=True. + A POST HTTPS request is accepted when USE_X_FORWARDED_*=True. """ self._test_https_good_referer_behind_proxy() diff --git a/tests/requests/tests.py b/tests/requests/tests.py index b6b1db4273..6fbcd13819 100644 --- a/tests/requests/tests.py +++ b/tests/requests/tests.py @@ -792,7 +792,8 @@ class HostValidationTests(SimpleTestCase): 'HTTP_X_FORWARDED_HOST': 'example.com:8000', } # Should use the X-Forwarded-Host header - self.assertEqual(request.get_port(), '8000') + with override_settings(ALLOWED_HOSTS=['example.com']): + self.assertEqual(request.get_port(), '8000') request = HttpRequest() request.META = { @@ -810,7 +811,8 @@ class HostValidationTests(SimpleTestCase): 'HTTP_X_FORWARDED_HOST': '[2001:19f0:feee::dead:beef:cafe]:8000', } # Should use the X-Forwarded-Host header - self.assertEqual(request.get_port(), '8000') + with override_settings(ALLOWED_HOSTS=['[2001:19f0:feee::dead:beef:cafe]']): + self.assertEqual(request.get_port(), '8000') request = HttpRequest() request.META = { @@ -829,8 +831,9 @@ class HostValidationTests(SimpleTestCase): 'HTTP_X_FORWARDED_HOST': 'example.com', 'HTTP_X_FORWARDED_PORT': '8010', } - # Should use the X-Forwarded-Port header - self.assertEqual(request.get_port(), '8010') + with override_settings(ALLOWED_HOSTS=['example.com']): + # Should use the X-Forwarded-Port header + self.assertEqual(request.get_port(), '8010') request = HttpRequest() request.META = {