mirror of
https://github.com/django/django.git
synced 2025-09-26 12:09:19 +00:00
[1.4.x] Added ALLOWED_HOSTS setting for HTTP host header validation.
This is a security fix; disclosure and advisory coming shortly.
This commit is contained in:
parent
57b62a74cb
commit
9936fdb11d
13 changed files with 314 additions and 175 deletions
|
@ -7,6 +7,7 @@ from django.http import HttpRequest, HttpResponse
|
|||
from django.middleware.csrf import CsrfViewMiddleware, CSRF_KEY_LENGTH
|
||||
from django.template import RequestContext, Template
|
||||
from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
from django.views.decorators.csrf import csrf_exempt, requires_csrf_token, ensure_csrf_cookie
|
||||
|
||||
|
||||
|
@ -267,6 +268,7 @@ class CsrfViewMiddlewareTest(TestCase):
|
|||
csrf_cookie = resp2.cookies[settings.CSRF_COOKIE_NAME]
|
||||
self._check_token_present(resp, csrf_id=csrf_cookie.value)
|
||||
|
||||
@override_settings(ALLOWED_HOSTS=['www.example.com'])
|
||||
def test_https_bad_referer(self):
|
||||
"""
|
||||
Test that a POST HTTPS request with a bad referer is rejected
|
||||
|
@ -279,6 +281,7 @@ class CsrfViewMiddlewareTest(TestCase):
|
|||
self.assertNotEqual(None, req2)
|
||||
self.assertEqual(403, req2.status_code)
|
||||
|
||||
@override_settings(ALLOWED_HOSTS=['www.example.com'])
|
||||
def test_https_good_referer(self):
|
||||
"""
|
||||
Test that a POST HTTPS request with a good referer is accepted
|
||||
|
@ -290,6 +293,7 @@ class CsrfViewMiddlewareTest(TestCase):
|
|||
req2 = CsrfViewMiddleware().process_view(req, post_form_view, (), {})
|
||||
self.assertEqual(None, req2)
|
||||
|
||||
@override_settings(ALLOWED_HOSTS=['www.example.com'])
|
||||
def test_https_good_referer_2(self):
|
||||
"""
|
||||
Test that a POST HTTPS request with a good referer is accepted
|
||||
|
|
|
@ -14,7 +14,7 @@ from django.core.exceptions import SuspiciousOperation
|
|||
from django.core.handlers.wsgi import WSGIRequest, LimitedStream
|
||||
from django.http import HttpRequest, HttpResponse, parse_cookie, build_request_repr, UnreadablePostError
|
||||
from django.test import TransactionTestCase
|
||||
from django.test.utils import get_warnings_state, restore_warnings_state
|
||||
from django.test.utils import get_warnings_state, restore_warnings_state, override_settings
|
||||
from django.utils import unittest
|
||||
from django.utils.http import cookie_date
|
||||
from django.utils.timezone import utc
|
||||
|
@ -109,161 +109,168 @@ class RequestsTests(unittest.TestCase):
|
|||
self.assertEqual(request.build_absolute_uri(location="/path/with:colons"),
|
||||
'http://www.example.com/path/with:colons')
|
||||
|
||||
@override_settings(
|
||||
USE_X_FORWARDED_HOST=False,
|
||||
ALLOWED_HOSTS=[
|
||||
'forward.com', 'example.com', 'internal.com', '12.34.56.78',
|
||||
'[2001:19f0:feee::dead:beef:cafe]', 'xn--4ca9at.com',
|
||||
'.multitenant.com', 'INSENSITIVE.com',
|
||||
])
|
||||
def test_http_get_host(self):
|
||||
old_USE_X_FORWARDED_HOST = settings.USE_X_FORWARDED_HOST
|
||||
try:
|
||||
settings.USE_X_FORWARDED_HOST = False
|
||||
# Check if X_FORWARDED_HOST is provided.
|
||||
request = HttpRequest()
|
||||
request.META = {
|
||||
'HTTP_X_FORWARDED_HOST': 'forward.com',
|
||||
'HTTP_HOST': 'example.com',
|
||||
'SERVER_NAME': 'internal.com',
|
||||
'SERVER_PORT': 80,
|
||||
}
|
||||
# X_FORWARDED_HOST is ignored.
|
||||
self.assertEqual(request.get_host(), 'example.com')
|
||||
|
||||
# Check if X_FORWARDED_HOST is provided.
|
||||
# Check if X_FORWARDED_HOST isn't provided.
|
||||
request = HttpRequest()
|
||||
request.META = {
|
||||
'HTTP_HOST': 'example.com',
|
||||
'SERVER_NAME': 'internal.com',
|
||||
'SERVER_PORT': 80,
|
||||
}
|
||||
self.assertEqual(request.get_host(), 'example.com')
|
||||
|
||||
# Check if HTTP_HOST isn't provided.
|
||||
request = HttpRequest()
|
||||
request.META = {
|
||||
'SERVER_NAME': 'internal.com',
|
||||
'SERVER_PORT': 80,
|
||||
}
|
||||
self.assertEqual(request.get_host(), 'internal.com')
|
||||
|
||||
# Check if HTTP_HOST isn't provided, and we're on a nonstandard port
|
||||
request = HttpRequest()
|
||||
request.META = {
|
||||
'SERVER_NAME': 'internal.com',
|
||||
'SERVER_PORT': 8042,
|
||||
}
|
||||
self.assertEqual(request.get_host(), 'internal.com:8042')
|
||||
|
||||
# Poisoned host headers are rejected as suspicious
|
||||
legit_hosts = [
|
||||
'example.com',
|
||||
'example.com:80',
|
||||
'12.34.56.78',
|
||||
'12.34.56.78:443',
|
||||
'[2001:19f0:feee::dead:beef:cafe]',
|
||||
'[2001:19f0:feee::dead:beef:cafe]:8080',
|
||||
'xn--4ca9at.com', # Punnycode for öäü.com
|
||||
'anything.multitenant.com',
|
||||
'multitenant.com',
|
||||
'insensitive.com',
|
||||
]
|
||||
|
||||
poisoned_hosts = [
|
||||
'example.com@evil.tld',
|
||||
'example.com:dr.frankenstein@evil.tld',
|
||||
'example.com:dr.frankenstein@evil.tld:80',
|
||||
'example.com:80/badpath',
|
||||
'example.com: recovermypassword.com',
|
||||
'other.com', # not in ALLOWED_HOSTS
|
||||
]
|
||||
|
||||
for host in legit_hosts:
|
||||
request = HttpRequest()
|
||||
request.META = {
|
||||
u'HTTP_X_FORWARDED_HOST': u'forward.com',
|
||||
u'HTTP_HOST': u'example.com',
|
||||
u'SERVER_NAME': u'internal.com',
|
||||
u'SERVER_PORT': 80,
|
||||
'HTTP_HOST': host,
|
||||
}
|
||||
# X_FORWARDED_HOST is ignored.
|
||||
self.assertEqual(request.get_host(), 'example.com')
|
||||
request.get_host()
|
||||
|
||||
# Check if X_FORWARDED_HOST isn't provided.
|
||||
request = HttpRequest()
|
||||
request.META = {
|
||||
u'HTTP_HOST': u'example.com',
|
||||
u'SERVER_NAME': u'internal.com',
|
||||
u'SERVER_PORT': 80,
|
||||
}
|
||||
self.assertEqual(request.get_host(), 'example.com')
|
||||
|
||||
# Check if HTTP_HOST isn't provided.
|
||||
request = HttpRequest()
|
||||
request.META = {
|
||||
u'SERVER_NAME': u'internal.com',
|
||||
u'SERVER_PORT': 80,
|
||||
}
|
||||
self.assertEqual(request.get_host(), 'internal.com')
|
||||
|
||||
# Check if HTTP_HOST isn't provided, and we're on a nonstandard port
|
||||
request = HttpRequest()
|
||||
request.META = {
|
||||
u'SERVER_NAME': u'internal.com',
|
||||
u'SERVER_PORT': 8042,
|
||||
}
|
||||
self.assertEqual(request.get_host(), 'internal.com:8042')
|
||||
|
||||
# Poisoned host headers are rejected as suspicious
|
||||
legit_hosts = [
|
||||
'example.com',
|
||||
'example.com:80',
|
||||
'12.34.56.78',
|
||||
'12.34.56.78:443',
|
||||
'[2001:19f0:feee::dead:beef:cafe]',
|
||||
'[2001:19f0:feee::dead:beef:cafe]:8080',
|
||||
'xn--4ca9at.com', # Punnycode for öäü.com
|
||||
]
|
||||
|
||||
poisoned_hosts = [
|
||||
'example.com@evil.tld',
|
||||
'example.com:dr.frankenstein@evil.tld',
|
||||
'example.com:dr.frankenstein@evil.tld:80',
|
||||
'example.com:80/badpath',
|
||||
'example.com: recovermypassword.com',
|
||||
]
|
||||
|
||||
for host in legit_hosts:
|
||||
for host in poisoned_hosts:
|
||||
with self.assertRaises(SuspiciousOperation):
|
||||
request = HttpRequest()
|
||||
request.META = {
|
||||
'HTTP_HOST': host,
|
||||
}
|
||||
request.get_host()
|
||||
|
||||
for host in poisoned_hosts:
|
||||
with self.assertRaises(SuspiciousOperation):
|
||||
request = HttpRequest()
|
||||
request.META = {
|
||||
'HTTP_HOST': host,
|
||||
}
|
||||
request.get_host()
|
||||
|
||||
finally:
|
||||
settings.USE_X_FORWARDED_HOST = old_USE_X_FORWARDED_HOST
|
||||
|
||||
@override_settings(USE_X_FORWARDED_HOST=True, ALLOWED_HOSTS=['*'])
|
||||
def test_http_get_host_with_x_forwarded_host(self):
|
||||
old_USE_X_FORWARDED_HOST = settings.USE_X_FORWARDED_HOST
|
||||
try:
|
||||
settings.USE_X_FORWARDED_HOST = True
|
||||
# Check if X_FORWARDED_HOST is provided.
|
||||
request = HttpRequest()
|
||||
request.META = {
|
||||
'HTTP_X_FORWARDED_HOST': 'forward.com',
|
||||
'HTTP_HOST': 'example.com',
|
||||
'SERVER_NAME': 'internal.com',
|
||||
'SERVER_PORT': 80,
|
||||
}
|
||||
# X_FORWARDED_HOST is obeyed.
|
||||
self.assertEqual(request.get_host(), 'forward.com')
|
||||
|
||||
# Check if X_FORWARDED_HOST is provided.
|
||||
# Check if X_FORWARDED_HOST isn't provided.
|
||||
request = HttpRequest()
|
||||
request.META = {
|
||||
'HTTP_HOST': 'example.com',
|
||||
'SERVER_NAME': 'internal.com',
|
||||
'SERVER_PORT': 80,
|
||||
}
|
||||
self.assertEqual(request.get_host(), 'example.com')
|
||||
|
||||
# Check if HTTP_HOST isn't provided.
|
||||
request = HttpRequest()
|
||||
request.META = {
|
||||
'SERVER_NAME': 'internal.com',
|
||||
'SERVER_PORT': 80,
|
||||
}
|
||||
self.assertEqual(request.get_host(), 'internal.com')
|
||||
|
||||
# Check if HTTP_HOST isn't provided, and we're on a nonstandard port
|
||||
request = HttpRequest()
|
||||
request.META = {
|
||||
'SERVER_NAME': 'internal.com',
|
||||
'SERVER_PORT': 8042,
|
||||
}
|
||||
self.assertEqual(request.get_host(), 'internal.com:8042')
|
||||
|
||||
# Poisoned host headers are rejected as suspicious
|
||||
legit_hosts = [
|
||||
'example.com',
|
||||
'example.com:80',
|
||||
'12.34.56.78',
|
||||
'12.34.56.78:443',
|
||||
'[2001:19f0:feee::dead:beef:cafe]',
|
||||
'[2001:19f0:feee::dead:beef:cafe]:8080',
|
||||
'xn--4ca9at.com', # Punnycode for öäü.com
|
||||
]
|
||||
|
||||
poisoned_hosts = [
|
||||
'example.com@evil.tld',
|
||||
'example.com:dr.frankenstein@evil.tld',
|
||||
'example.com:dr.frankenstein@evil.tld:80',
|
||||
'example.com:80/badpath',
|
||||
'example.com: recovermypassword.com',
|
||||
]
|
||||
|
||||
for host in legit_hosts:
|
||||
request = HttpRequest()
|
||||
request.META = {
|
||||
u'HTTP_X_FORWARDED_HOST': u'forward.com',
|
||||
u'HTTP_HOST': u'example.com',
|
||||
u'SERVER_NAME': u'internal.com',
|
||||
u'SERVER_PORT': 80,
|
||||
'HTTP_HOST': host,
|
||||
}
|
||||
# X_FORWARDED_HOST is obeyed.
|
||||
self.assertEqual(request.get_host(), 'forward.com')
|
||||
request.get_host()
|
||||
|
||||
# Check if X_FORWARDED_HOST isn't provided.
|
||||
request = HttpRequest()
|
||||
request.META = {
|
||||
u'HTTP_HOST': u'example.com',
|
||||
u'SERVER_NAME': u'internal.com',
|
||||
u'SERVER_PORT': 80,
|
||||
}
|
||||
self.assertEqual(request.get_host(), 'example.com')
|
||||
|
||||
# Check if HTTP_HOST isn't provided.
|
||||
request = HttpRequest()
|
||||
request.META = {
|
||||
u'SERVER_NAME': u'internal.com',
|
||||
u'SERVER_PORT': 80,
|
||||
}
|
||||
self.assertEqual(request.get_host(), 'internal.com')
|
||||
|
||||
# Check if HTTP_HOST isn't provided, and we're on a nonstandard port
|
||||
request = HttpRequest()
|
||||
request.META = {
|
||||
u'SERVER_NAME': u'internal.com',
|
||||
u'SERVER_PORT': 8042,
|
||||
}
|
||||
self.assertEqual(request.get_host(), 'internal.com:8042')
|
||||
|
||||
# Poisoned host headers are rejected as suspicious
|
||||
legit_hosts = [
|
||||
'example.com',
|
||||
'example.com:80',
|
||||
'12.34.56.78',
|
||||
'12.34.56.78:443',
|
||||
'[2001:19f0:feee::dead:beef:cafe]',
|
||||
'[2001:19f0:feee::dead:beef:cafe]:8080',
|
||||
'xn--4ca9at.com', # Punnycode for öäü.com
|
||||
]
|
||||
|
||||
poisoned_hosts = [
|
||||
'example.com@evil.tld',
|
||||
'example.com:dr.frankenstein@evil.tld',
|
||||
'example.com:dr.frankenstein@evil.tld:80',
|
||||
'example.com:80/badpath',
|
||||
'example.com: recovermypassword.com',
|
||||
]
|
||||
|
||||
for host in legit_hosts:
|
||||
for host in poisoned_hosts:
|
||||
with self.assertRaises(SuspiciousOperation):
|
||||
request = HttpRequest()
|
||||
request.META = {
|
||||
'HTTP_HOST': host,
|
||||
}
|
||||
request.get_host()
|
||||
|
||||
for host in poisoned_hosts:
|
||||
with self.assertRaises(SuspiciousOperation):
|
||||
request = HttpRequest()
|
||||
request.META = {
|
||||
'HTTP_HOST': host,
|
||||
}
|
||||
request.get_host()
|
||||
|
||||
finally:
|
||||
settings.USE_X_FORWARDED_HOST = old_USE_X_FORWARDED_HOST
|
||||
@override_settings(DEBUG=True, ALLOWED_HOSTS=[])
|
||||
def test_host_validation_disabled_in_debug_mode(self):
|
||||
"""If ALLOWED_HOSTS is empty and DEBUG is True, all hosts pass."""
|
||||
request = HttpRequest()
|
||||
request.META = {
|
||||
'HTTP_HOST': 'example.com',
|
||||
}
|
||||
self.assertEqual(request.get_host(), 'example.com')
|
||||
|
||||
def test_near_expiration(self):
|
||||
"Cookie will expire when an near expiration time is provided"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue