1
0
mirror of https://github.com/django/django.git synced 2025-06-05 11:39:13 +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 codecs
import copy import copy
import operator import warnings
from collections import namedtuple
from io import BytesIO from io import BytesIO
from itertools import chain from itertools import chain
from urllib.parse import parse_qsl, quote, urlencode, urljoin, urlsplit 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 from django.utils.regex_helper import _lazy_re_compile
RAISE_ERROR = object() 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): class UnreadablePostError(OSError):
@ -143,60 +146,62 @@ class HttpRequest:
else: else:
self.encoding = self.content_params["charset"] self.encoding = self.content_params["charset"]
@cached_property def _get_parsed_host_header(self, validate=True):
def _parsed_host_header(self): if not hasattr(self, '_parsed_host_obj'):
use_x_fw_host = settings.USE_X_FORWARDED_HOST use_x_fw_host = settings.USE_X_FORWARDED_HOST
use_x_fw_port = settings.USE_X_FORWARDED_PORT use_x_fw_port = settings.USE_X_FORWARDED_PORT
port_in_x_fw_host = False port_in_x_fw_host = False
default_port = ('443' if self.is_secure() else '80') default_port = ('443' if self.is_secure() else '80')
if use_x_fw_host and 'HTTP_X_FORWARDED_HOST' in self.META: if use_x_fw_host and 'HTTP_X_FORWARDED_HOST' in self.META:
host, port = _parse_host_header(self.META['HTTP_X_FORWARDED_HOST']) host, port = _parse_host_header(self.META['HTTP_X_FORWARDED_HOST'])
port_in_x_fw_host = port != '' port_in_x_fw_host = port != ''
elif 'HTTP_HOST' in self.META: elif 'HTTP_HOST' in self.META:
host, port = _parse_host_header(self.META['HTTP_HOST']) 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: else:
# Reconstruct the host using the algorithm from PEP 333. parsed_host = self._parsed_host_obj
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
# Allow variants of localhost if ALLOWED_HOSTS is empty and DEBUG=True. # Allow variants of localhost if ALLOWED_HOSTS is empty and DEBUG=True.
allowed_hosts = settings.ALLOWED_HOSTS allowed_hosts = settings.ALLOWED_HOSTS
if settings.DEBUG and not allowed_hosts: if settings.DEBUG and not allowed_hosts:
allowed_hosts = [".localhost", "127.0.0.1", "[::1]"] allowed_hosts = [".localhost", "127.0.0.1", "[::1]"]
domain, port = split_domain_port(host_header) msg = "Invalid HTTP_HOST header: %r." % parsed_host.combined
if domain and validate_host(domain, allowed_hosts): if validate and not (parsed_host.domain and validate_host(parsed_host.domain, allowed_hosts)):
return host_header if parsed_host.domain:
else: msg += " You may need to add %r to ALLOWED_HOSTS." % parsed_host.domain
msg = "Invalid HTTP_HOST header: %r." % host_header
if domain:
msg += " You may need to add %r to ALLOWED_HOSTS." % domain
else: else:
msg += ( msg += (
" The domain name provided is not valid according to RFC 1034/1035." " The domain name provided is not valid according to RFC 1034/1035."
) )
raise DisallowedHost(msg) 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): def get_port(self):
"""Return the port number for the request as a string.""" """Return the port number for the request as a string."""
_, port, _ = self._parsed_host_header return self._get_parsed_host_header().port
return port
def get_full_path(self, force_append_slash=False): def get_full_path(self, force_append_slash=False):
return self._get_full_path(self.path, force_append_slash) 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 Return an absolute URI from variables available in this request. Skip
allowed hosts protection, so may return insecure URI. allowed hosts protection, so may return insecure URI.
""" """
_, _, host_header = self._parsed_host_header
return '{scheme}://{host}{path}'.format( return '{scheme}://{host}{path}'.format(
scheme=self.scheme, scheme=self.scheme,
host=host_header, host=self._get_parsed_host_header(validate=False).combined,
path=self.get_full_path(), path=self.get_full_path(),
) )

View File

@ -1274,28 +1274,15 @@ class CsrfViewMiddlewareTests(CsrfViewMiddlewareTestMixin, SimpleTestCase):
self.assertEqual(csrf_cookie, TEST_SECRET) self.assertEqual(csrf_cookie, TEST_SECRET)
self._check_token_present(resp, csrf_cookie) 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( @override_settings(
ALLOWED_HOSTS=["www.example.com"], ALLOWED_HOSTS=['www.example.com'],
CSRF_COOKIE_DOMAIN=".example.com", CSRF_COOKIE_DOMAIN='.example.com',
USE_X_FORWARDED_PORT=True, USE_X_FORWARDED_PORT=True,
USE_X_FORWARDED_HOST=True
) )
def test_https_good_referer_behind_proxy(self): 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() self._test_https_good_referer_behind_proxy()
@ -1428,11 +1415,12 @@ class CsrfViewMiddlewareUseSessionsTests(CsrfViewMiddlewareTestMixin, SimpleTest
ALLOWED_HOSTS=["www.example.com"], ALLOWED_HOSTS=["www.example.com"],
SESSION_COOKIE_DOMAIN=".example.com", SESSION_COOKIE_DOMAIN=".example.com",
USE_X_FORWARDED_PORT=True, USE_X_FORWARDED_PORT=True,
USE_X_FORWARDED_HOST=True,
DEBUG=True, DEBUG=True,
) )
def test_https_good_referer_behind_proxy(self): 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() self._test_https_good_referer_behind_proxy()

View File

@ -792,7 +792,8 @@ class HostValidationTests(SimpleTestCase):
'HTTP_X_FORWARDED_HOST': 'example.com:8000', 'HTTP_X_FORWARDED_HOST': 'example.com:8000',
} }
# Should use the X-Forwarded-Host header # 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 = HttpRequest()
request.META = { request.META = {
@ -810,7 +811,8 @@ class HostValidationTests(SimpleTestCase):
'HTTP_X_FORWARDED_HOST': '[2001:19f0:feee::dead:beef:cafe]:8000', 'HTTP_X_FORWARDED_HOST': '[2001:19f0:feee::dead:beef:cafe]:8000',
} }
# Should use the X-Forwarded-Host header # 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 = HttpRequest()
request.META = { request.META = {
@ -829,8 +831,9 @@ class HostValidationTests(SimpleTestCase):
'HTTP_X_FORWARDED_HOST': 'example.com', 'HTTP_X_FORWARDED_HOST': 'example.com',
'HTTP_X_FORWARDED_PORT': '8010', 'HTTP_X_FORWARDED_PORT': '8010',
} }
# Should use the X-Forwarded-Port header with override_settings(ALLOWED_HOSTS=['example.com']):
self.assertEqual(request.get_port(), '8010') # Should use the X-Forwarded-Port header
self.assertEqual(request.get_port(), '8010')
request = HttpRequest() request = HttpRequest()
request.META = { request.META = {