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:
parent
ef2118775f
commit
22f0f275f0
@ -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(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -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 = {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user