mirror of
https://github.com/django/django.git
synced 2025-06-05 19:49:13 +00:00
Fixed #31354: Properly reconstruct host header in presence of X-Forwarded-* header.
This commit is contained in:
parent
4c452cc377
commit
ef2118775f
@ -31,9 +31,7 @@ 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(
|
host_validation_re = _lazy_re_compile(r"^([a-z0-9.-]+|\[[a-f0-9]*:[a-f0-9\.:]+\])(?::(\d+))?$")
|
||||||
r"^([a-z0-9.-]+|\[[a-f0-9]*:[a-f0-9.:]+\])(?::([0-9]+))?$"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class UnreadablePostError(OSError):
|
class UnreadablePostError(OSError):
|
||||||
@ -145,38 +143,48 @@ class HttpRequest:
|
|||||||
else:
|
else:
|
||||||
self.encoding = self.content_params["charset"]
|
self.encoding = self.content_params["charset"]
|
||||||
|
|
||||||
def _get_raw_host(self):
|
@cached_property
|
||||||
"""
|
def _parsed_host_header(self):
|
||||||
Return the HTTP host using the environment or request headers. Skip
|
use_x_fw_host = settings.USE_X_FORWARDED_HOST
|
||||||
allowed hosts protection, so may return an insecure host.
|
use_x_fw_port = settings.USE_X_FORWARDED_PORT
|
||||||
"""
|
|
||||||
# We try three options, in order of decreasing preference.
|
port_in_x_fw_host = False
|
||||||
if settings.USE_X_FORWARDED_HOST and ("HTTP_X_FORWARDED_HOST" in self.META):
|
default_port = ('443' if self.is_secure() else '80')
|
||||||
host = self.META["HTTP_X_FORWARDED_HOST"]
|
|
||||||
elif "HTTP_HOST" in self.META:
|
if use_x_fw_host and 'HTTP_X_FORWARDED_HOST' in self.META:
|
||||||
host = self.META["HTTP_HOST"]
|
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:
|
else:
|
||||||
# Reconstruct the host using the algorithm from PEP 333.
|
# Reconstruct the host using the algorithm from PEP 333.
|
||||||
host = self.META["SERVER_NAME"]
|
host, port = self.META['SERVER_NAME'], str(self.META['SERVER_PORT'])
|
||||||
server_port = self.get_port()
|
if port == default_port:
|
||||||
if server_port != ("443" if self.is_secure() else "80"):
|
port = ''
|
||||||
host = "%s:%s" % (host, server_port)
|
|
||||||
return host
|
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):
|
def get_host(self):
|
||||||
"""Return the HTTP host using the environment or request headers."""
|
"""Return the HTTP host using the environment or request headers."""
|
||||||
host = self._get_raw_host()
|
_, _, 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)
|
domain, port = split_domain_port(host_header)
|
||||||
if domain and validate_host(domain, allowed_hosts):
|
if domain and validate_host(domain, allowed_hosts):
|
||||||
return host
|
return host_header
|
||||||
else:
|
else:
|
||||||
msg = "Invalid HTTP_HOST header: %r." % host
|
msg = "Invalid HTTP_HOST header: %r." % host_header
|
||||||
if domain:
|
if domain:
|
||||||
msg += " You may need to add %r to ALLOWED_HOSTS." % domain
|
msg += " You may need to add %r to ALLOWED_HOSTS." % domain
|
||||||
else:
|
else:
|
||||||
@ -187,11 +195,8 @@ class HttpRequest:
|
|||||||
|
|
||||||
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."""
|
||||||
if settings.USE_X_FORWARDED_PORT and "HTTP_X_FORWARDED_PORT" in self.META:
|
_, port, _ = self._parsed_host_header
|
||||||
port = self.META["HTTP_X_FORWARDED_PORT"]
|
return port
|
||||||
else:
|
|
||||||
port = self.META["SERVER_PORT"]
|
|
||||||
return str(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)
|
||||||
@ -236,6 +241,18 @@ class HttpRequest:
|
|||||||
raise
|
raise
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
def get_raw_uri(self):
|
||||||
|
"""
|
||||||
|
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,
|
||||||
|
path=self.get_full_path(),
|
||||||
|
)
|
||||||
|
|
||||||
def build_absolute_uri(self, location=None):
|
def build_absolute_uri(self, location=None):
|
||||||
"""
|
"""
|
||||||
Build an absolute URI from the location and the variables available in
|
Build an absolute URI from the location and the variables available in
|
||||||
@ -763,6 +780,20 @@ def bytes_to_text(s, encoding):
|
|||||||
return s
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_host_header(host_header):
|
||||||
|
"""
|
||||||
|
Returns a (domain, port) tuple for a given host.
|
||||||
|
|
||||||
|
Neither domain name nor port are validated.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if host_header[-1] == ']':
|
||||||
|
# It's an IPv6 address without a port.
|
||||||
|
return host_header, ''
|
||||||
|
bits = host_header.rsplit(':', 1)
|
||||||
|
return tuple(bits) if len(bits) == 2 else (bits[0], '')
|
||||||
|
|
||||||
|
|
||||||
def split_domain_port(host):
|
def split_domain_port(host):
|
||||||
"""
|
"""
|
||||||
Return a (domain, port) tuple from a given host.
|
Return a (domain, port) tuple from a given host.
|
||||||
@ -770,11 +801,17 @@ def split_domain_port(host):
|
|||||||
Returned domain is lowercased. If the host is invalid, the domain will be
|
Returned domain is lowercased. If the host is invalid, the domain will be
|
||||||
empty.
|
empty.
|
||||||
"""
|
"""
|
||||||
if match := host_validation_re.fullmatch(host.lower()):
|
host = host.lower()
|
||||||
domain, port = match.groups(default="")
|
|
||||||
# Remove a trailing dot (if present) from the domain.
|
host_match = host_validation_re.match(host)
|
||||||
return domain.removesuffix("."), port
|
if not host_match:
|
||||||
return "", ""
|
return '', ''
|
||||||
|
|
||||||
|
domain, port = host_match.groups()
|
||||||
|
port = port or ''
|
||||||
|
# Remove a trailing dot (if present) from the domain.
|
||||||
|
domain = domain[:-1] if domain.endswith('.') else domain
|
||||||
|
return domain, port
|
||||||
|
|
||||||
|
|
||||||
def validate_host(host, allowed_hosts):
|
def validate_host(host, allowed_hosts):
|
||||||
|
@ -831,8 +831,8 @@ class CsrfViewMiddlewareTestMixin(CsrfFunctionTestMixin):
|
|||||||
def _test_https_good_referer_matches_cookie_domain(self):
|
def _test_https_good_referer_matches_cookie_domain(self):
|
||||||
req = self._get_POST_request_with_token()
|
req = self._get_POST_request_with_token()
|
||||||
req._is_secure_override = True
|
req._is_secure_override = True
|
||||||
req.META["HTTP_REFERER"] = "https://foo.example.com/"
|
req.META['HTTP_HOST'] = 'www.example.com'
|
||||||
req.META["SERVER_PORT"] = "443"
|
req.META['HTTP_REFERER'] = 'https://foo.example.com/'
|
||||||
mw = CsrfViewMiddleware(post_form_view)
|
mw = CsrfViewMiddleware(post_form_view)
|
||||||
mw.process_request(req)
|
mw.process_request(req)
|
||||||
response = mw.process_view(req, post_form_view, (), {})
|
response = mw.process_view(req, post_form_view, (), {})
|
||||||
@ -841,9 +841,8 @@ class CsrfViewMiddlewareTestMixin(CsrfFunctionTestMixin):
|
|||||||
def _test_https_good_referer_matches_cookie_domain_with_different_port(self):
|
def _test_https_good_referer_matches_cookie_domain_with_different_port(self):
|
||||||
req = self._get_POST_request_with_token()
|
req = self._get_POST_request_with_token()
|
||||||
req._is_secure_override = True
|
req._is_secure_override = True
|
||||||
req.META["HTTP_HOST"] = "www.example.com"
|
req.META['HTTP_HOST'] = 'www.example.com:4443'
|
||||||
req.META["HTTP_REFERER"] = "https://foo.example.com:4443/"
|
req.META['HTTP_REFERER'] = 'https://foo.example.com:4443/'
|
||||||
req.META["SERVER_PORT"] = "4443"
|
|
||||||
mw = CsrfViewMiddleware(post_form_view)
|
mw = CsrfViewMiddleware(post_form_view)
|
||||||
mw.process_request(req)
|
mw.process_request(req)
|
||||||
response = mw.process_view(req, post_form_view, (), {})
|
response = mw.process_view(req, post_form_view, (), {})
|
||||||
|
1053
tests/requests/tests.py
Normal file
1053
tests/requests/tests.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -106,37 +106,42 @@ class SitesFrameworkTests(TestCase):
|
|||||||
|
|
||||||
@override_settings(SITE_ID=None, ALLOWED_HOSTS=["example.com", "example.net"])
|
@override_settings(SITE_ID=None, ALLOWED_HOSTS=["example.com", "example.net"])
|
||||||
def test_get_current_site_no_site_id_and_handle_port_fallback(self):
|
def test_get_current_site_no_site_id_and_handle_port_fallback(self):
|
||||||
request = HttpRequest()
|
|
||||||
s1 = self.site
|
s1 = self.site
|
||||||
s2 = Site.objects.create(domain="example.com:80", name="example.com:80")
|
s2 = Site.objects.create(domain="example.com:80", name="example.com:80")
|
||||||
|
|
||||||
# Host header without port
|
# Host header without port
|
||||||
request.META = {"HTTP_HOST": "example.com"}
|
request = HttpRequest()
|
||||||
|
request.META = {'HTTP_HOST': 'example.com'}
|
||||||
site = get_current_site(request)
|
site = get_current_site(request)
|
||||||
self.assertEqual(site, s1)
|
self.assertEqual(site, s1)
|
||||||
|
|
||||||
# Host header with port - match, no fallback without port
|
# Host header with port - match, no fallback without port
|
||||||
request.META = {"HTTP_HOST": "example.com:80"}
|
request = HttpRequest()
|
||||||
|
request.META = {'HTTP_HOST': 'example.com:80'}
|
||||||
site = get_current_site(request)
|
site = get_current_site(request)
|
||||||
self.assertEqual(site, s2)
|
self.assertEqual(site, s2)
|
||||||
|
|
||||||
# Host header with port - no match, fallback without port
|
# Host header with port - no match, fallback without port
|
||||||
request.META = {"HTTP_HOST": "example.com:81"}
|
request = HttpRequest()
|
||||||
|
request.META = {'HTTP_HOST': 'example.com:81'}
|
||||||
site = get_current_site(request)
|
site = get_current_site(request)
|
||||||
self.assertEqual(site, s1)
|
self.assertEqual(site, s1)
|
||||||
|
|
||||||
# Host header with non-matching domain
|
# Host header with non-matching domain
|
||||||
request.META = {"HTTP_HOST": "example.net"}
|
request = HttpRequest()
|
||||||
|
request.META = {'HTTP_HOST': 'example.net'}
|
||||||
with self.assertRaises(ObjectDoesNotExist):
|
with self.assertRaises(ObjectDoesNotExist):
|
||||||
get_current_site(request)
|
get_current_site(request)
|
||||||
|
|
||||||
# Ensure domain for RequestSite always matches host header
|
# Ensure domain for RequestSite always matches host header
|
||||||
with self.modify_settings(INSTALLED_APPS={"remove": "django.contrib.sites"}):
|
with self.modify_settings(INSTALLED_APPS={'remove': 'django.contrib.sites'}):
|
||||||
request.META = {"HTTP_HOST": "example.com"}
|
request = HttpRequest()
|
||||||
|
request.META = {'HTTP_HOST': 'example.com'}
|
||||||
site = get_current_site(request)
|
site = get_current_site(request)
|
||||||
self.assertEqual(site.name, "example.com")
|
self.assertEqual(site.name, "example.com")
|
||||||
|
|
||||||
request.META = {"HTTP_HOST": "example.com:80"}
|
request = HttpRequest()
|
||||||
|
request.META = {'HTTP_HOST': 'example.com:80'}
|
||||||
site = get_current_site(request)
|
site = get_current_site(request)
|
||||||
self.assertEqual(site.name, "example.com:80")
|
self.assertEqual(site.name, "example.com:80")
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user