1
0
mirror of https://github.com/django/django.git synced 2025-04-03 13:06:49 +00:00

Moved host header parsing into a function that optionally performs validation too.

This commit is contained in:
Florian Apolloner 2020-06-14 11:22:22 +02:00 committed by GappleBee
parent ef2118775f
commit 22f0f275f0
3 changed files with 58 additions and 63 deletions

View File

@ -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(),
)

View File

@ -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()

View File

@ -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 = {