2014-09-13 02:50:36 +08:00
|
|
|
from django.http import HttpResponse
|
2015-04-18 05:38:20 +08:00
|
|
|
from django.test import RequestFactory, SimpleTestCase
|
2014-09-13 02:50:36 +08:00
|
|
|
from django.test.utils import override_settings
|
|
|
|
|
|
|
|
|
2015-04-18 05:38:20 +08:00
|
|
|
class SecurityMiddlewareTest(SimpleTestCase):
|
2019-09-27 01:06:35 +08:00
|
|
|
def middleware(self, *args, **kwargs):
|
2014-09-13 02:50:36 +08:00
|
|
|
from django.middleware.security import SecurityMiddleware
|
2022-02-04 03:24:19 +08:00
|
|
|
|
2019-09-27 01:06:35 +08:00
|
|
|
return SecurityMiddleware(self.response(*args, **kwargs))
|
2014-09-13 02:50:36 +08:00
|
|
|
|
|
|
|
@property
|
|
|
|
def secure_request_kwargs(self):
|
|
|
|
return {"wsgi.url_scheme": "https"}
|
|
|
|
|
2017-02-02 00:41:56 +08:00
|
|
|
def response(self, *args, headers=None, **kwargs):
|
2019-09-27 01:06:35 +08:00
|
|
|
def get_response(req):
|
|
|
|
response = HttpResponse(*args, **kwargs)
|
|
|
|
if headers:
|
|
|
|
for k, v in headers.items():
|
2020-07-14 19:32:24 +08:00
|
|
|
response.headers[k] = v
|
2019-09-27 01:06:35 +08:00
|
|
|
return response
|
2022-02-04 03:24:19 +08:00
|
|
|
|
2019-09-27 01:06:35 +08:00
|
|
|
return get_response
|
2014-09-13 02:50:36 +08:00
|
|
|
|
2017-02-02 00:41:56 +08:00
|
|
|
def process_response(self, *args, secure=False, request=None, **kwargs):
|
2014-09-13 02:50:36 +08:00
|
|
|
request_kwargs = {}
|
2017-02-02 00:41:56 +08:00
|
|
|
if secure:
|
2014-09-13 02:50:36 +08:00
|
|
|
request_kwargs.update(self.secure_request_kwargs)
|
2017-02-02 00:41:56 +08:00
|
|
|
if request is None:
|
|
|
|
request = self.request.get("/some/url", **request_kwargs)
|
2019-09-27 01:06:35 +08:00
|
|
|
ret = self.middleware(*args, **kwargs).process_request(request)
|
2014-09-13 02:50:36 +08:00
|
|
|
if ret:
|
|
|
|
return ret
|
2019-09-27 01:06:35 +08:00
|
|
|
return self.middleware(*args, **kwargs)(request)
|
2014-09-13 02:50:36 +08:00
|
|
|
|
|
|
|
request = RequestFactory()
|
|
|
|
|
2017-02-02 00:41:56 +08:00
|
|
|
def process_request(self, method, *args, secure=False, **kwargs):
|
|
|
|
if secure:
|
2014-09-13 02:50:36 +08:00
|
|
|
kwargs.update(self.secure_request_kwargs)
|
|
|
|
req = getattr(self.request, method.lower())(*args, **kwargs)
|
2019-09-27 01:06:35 +08:00
|
|
|
return self.middleware().process_request(req)
|
2014-09-13 02:50:36 +08:00
|
|
|
|
|
|
|
@override_settings(SECURE_HSTS_SECONDS=3600)
|
|
|
|
def test_sts_on(self):
|
|
|
|
"""
|
2019-03-21 08:15:34 +08:00
|
|
|
With SECURE_HSTS_SECONDS=3600, the middleware adds
|
2018-10-30 06:19:04 +08:00
|
|
|
"Strict-Transport-Security: max-age=3600" to the response.
|
2014-09-13 02:50:36 +08:00
|
|
|
"""
|
|
|
|
self.assertEqual(
|
2020-07-14 19:32:24 +08:00
|
|
|
self.process_response(secure=True).headers["Strict-Transport-Security"],
|
2018-03-16 17:54:34 +08:00
|
|
|
"max-age=3600",
|
|
|
|
)
|
2014-09-13 02:50:36 +08:00
|
|
|
|
|
|
|
@override_settings(SECURE_HSTS_SECONDS=3600)
|
|
|
|
def test_sts_already_present(self):
|
|
|
|
"""
|
2018-10-30 06:19:04 +08:00
|
|
|
The middleware will not override a "Strict-Transport-Security" header
|
2014-09-13 02:50:36 +08:00
|
|
|
already present in the response.
|
|
|
|
"""
|
|
|
|
response = self.process_response(
|
2018-10-30 06:19:04 +08:00
|
|
|
secure=True, headers={"Strict-Transport-Security": "max-age=7200"}
|
|
|
|
)
|
2020-07-14 19:32:24 +08:00
|
|
|
self.assertEqual(response.headers["Strict-Transport-Security"], "max-age=7200")
|
2014-09-13 02:50:36 +08:00
|
|
|
|
2019-03-21 08:15:34 +08:00
|
|
|
@override_settings(SECURE_HSTS_SECONDS=3600)
|
2014-09-13 02:50:36 +08:00
|
|
|
def test_sts_only_if_secure(self):
|
|
|
|
"""
|
2018-10-30 06:19:04 +08:00
|
|
|
The "Strict-Transport-Security" header is not added to responses going
|
2014-09-13 02:50:36 +08:00
|
|
|
over an insecure connection.
|
|
|
|
"""
|
2020-07-14 19:32:24 +08:00
|
|
|
self.assertNotIn(
|
|
|
|
"Strict-Transport-Security",
|
|
|
|
self.process_response(secure=False).headers,
|
|
|
|
)
|
2014-09-13 02:50:36 +08:00
|
|
|
|
2019-03-21 08:15:34 +08:00
|
|
|
@override_settings(SECURE_HSTS_SECONDS=0)
|
2014-09-13 02:50:36 +08:00
|
|
|
def test_sts_off(self):
|
|
|
|
"""
|
2019-03-21 08:15:34 +08:00
|
|
|
With SECURE_HSTS_SECONDS=0, the middleware does not add a
|
2018-10-30 06:19:04 +08:00
|
|
|
"Strict-Transport-Security" header to the response.
|
2014-09-13 02:50:36 +08:00
|
|
|
"""
|
2020-07-14 19:32:24 +08:00
|
|
|
self.assertNotIn(
|
|
|
|
"Strict-Transport-Security",
|
|
|
|
self.process_response(secure=True).headers,
|
|
|
|
)
|
2014-09-13 02:50:36 +08:00
|
|
|
|
2019-03-21 08:15:34 +08:00
|
|
|
@override_settings(SECURE_HSTS_SECONDS=600, SECURE_HSTS_INCLUDE_SUBDOMAINS=True)
|
2014-09-13 02:50:36 +08:00
|
|
|
def test_sts_include_subdomains(self):
|
|
|
|
"""
|
2019-03-21 08:15:34 +08:00
|
|
|
With SECURE_HSTS_SECONDS non-zero and SECURE_HSTS_INCLUDE_SUBDOMAINS
|
2018-10-30 06:19:04 +08:00
|
|
|
True, the middleware adds a "Strict-Transport-Security" header with the
|
2016-07-29 00:30:16 +08:00
|
|
|
"includeSubDomains" directive to the response.
|
2014-09-13 02:50:36 +08:00
|
|
|
"""
|
|
|
|
response = self.process_response(secure=True)
|
2020-07-14 19:32:24 +08:00
|
|
|
self.assertEqual(
|
|
|
|
response.headers["Strict-Transport-Security"],
|
|
|
|
"max-age=600; includeSubDomains",
|
|
|
|
)
|
2014-09-13 02:50:36 +08:00
|
|
|
|
2019-03-21 08:15:34 +08:00
|
|
|
@override_settings(SECURE_HSTS_SECONDS=600, SECURE_HSTS_INCLUDE_SUBDOMAINS=False)
|
2014-09-13 02:50:36 +08:00
|
|
|
def test_sts_no_include_subdomains(self):
|
|
|
|
"""
|
2019-03-21 08:15:34 +08:00
|
|
|
With SECURE_HSTS_SECONDS non-zero and SECURE_HSTS_INCLUDE_SUBDOMAINS
|
2018-10-30 06:19:04 +08:00
|
|
|
False, the middleware adds a "Strict-Transport-Security" header without
|
2016-07-29 00:30:16 +08:00
|
|
|
the "includeSubDomains" directive to the response.
|
2014-09-13 02:50:36 +08:00
|
|
|
"""
|
|
|
|
response = self.process_response(secure=True)
|
2020-07-14 19:32:24 +08:00
|
|
|
self.assertEqual(response.headers["Strict-Transport-Security"], "max-age=600")
|
2014-09-13 02:50:36 +08:00
|
|
|
|
2016-07-29 00:48:07 +08:00
|
|
|
@override_settings(SECURE_HSTS_SECONDS=10886400, SECURE_HSTS_PRELOAD=True)
|
|
|
|
def test_sts_preload(self):
|
|
|
|
"""
|
2019-03-21 08:15:34 +08:00
|
|
|
With SECURE_HSTS_SECONDS non-zero and SECURE_HSTS_PRELOAD True, the
|
|
|
|
middleware adds a "Strict-Transport-Security" header with the "preload"
|
|
|
|
directive to the response.
|
2016-07-29 00:48:07 +08:00
|
|
|
"""
|
|
|
|
response = self.process_response(secure=True)
|
2020-07-14 19:32:24 +08:00
|
|
|
self.assertEqual(
|
|
|
|
response.headers["Strict-Transport-Security"],
|
|
|
|
"max-age=10886400; preload",
|
|
|
|
)
|
2016-07-29 00:48:07 +08:00
|
|
|
|
|
|
|
@override_settings(
|
|
|
|
SECURE_HSTS_SECONDS=10886400,
|
|
|
|
SECURE_HSTS_INCLUDE_SUBDOMAINS=True,
|
|
|
|
SECURE_HSTS_PRELOAD=True,
|
|
|
|
)
|
|
|
|
def test_sts_subdomains_and_preload(self):
|
|
|
|
"""
|
2019-03-21 08:15:34 +08:00
|
|
|
With SECURE_HSTS_SECONDS non-zero, SECURE_HSTS_INCLUDE_SUBDOMAINS and
|
2018-10-30 06:19:04 +08:00
|
|
|
SECURE_HSTS_PRELOAD True, the middleware adds a "Strict-Transport-Security"
|
2016-07-29 00:48:07 +08:00
|
|
|
header containing both the "includeSubDomains" and "preload" directives
|
|
|
|
to the response.
|
|
|
|
"""
|
|
|
|
response = self.process_response(secure=True)
|
2020-07-14 19:32:24 +08:00
|
|
|
self.assertEqual(
|
|
|
|
response.headers["Strict-Transport-Security"],
|
|
|
|
"max-age=10886400; includeSubDomains; preload",
|
|
|
|
)
|
2016-07-29 00:48:07 +08:00
|
|
|
|
|
|
|
@override_settings(SECURE_HSTS_SECONDS=10886400, SECURE_HSTS_PRELOAD=False)
|
|
|
|
def test_sts_no_preload(self):
|
|
|
|
"""
|
2019-03-21 08:15:34 +08:00
|
|
|
With SECURE_HSTS_SECONDS non-zero and SECURE_HSTS_PRELOAD
|
2018-10-30 06:19:04 +08:00
|
|
|
False, the middleware adds a "Strict-Transport-Security" header without
|
2016-07-29 00:48:07 +08:00
|
|
|
the "preload" directive to the response.
|
|
|
|
"""
|
|
|
|
response = self.process_response(secure=True)
|
2020-07-14 19:32:24 +08:00
|
|
|
self.assertEqual(
|
|
|
|
response.headers["Strict-Transport-Security"],
|
|
|
|
"max-age=10886400",
|
|
|
|
)
|
2016-07-29 00:48:07 +08:00
|
|
|
|
2014-09-13 02:50:36 +08:00
|
|
|
@override_settings(SECURE_CONTENT_TYPE_NOSNIFF=True)
|
|
|
|
def test_content_type_on(self):
|
|
|
|
"""
|
2019-03-21 08:15:34 +08:00
|
|
|
With SECURE_CONTENT_TYPE_NOSNIFF set to True, the middleware adds
|
2018-10-30 06:19:04 +08:00
|
|
|
"X-Content-Type-Options: nosniff" header to the response.
|
2014-09-13 02:50:36 +08:00
|
|
|
"""
|
2020-07-14 19:32:24 +08:00
|
|
|
self.assertEqual(
|
|
|
|
self.process_response().headers["X-Content-Type-Options"],
|
|
|
|
"nosniff",
|
|
|
|
)
|
2014-09-13 02:50:36 +08:00
|
|
|
|
2016-07-29 10:00:48 +08:00
|
|
|
@override_settings(SECURE_CONTENT_TYPE_NOSNIFF=True)
|
2014-09-13 02:50:36 +08:00
|
|
|
def test_content_type_already_present(self):
|
|
|
|
"""
|
2018-10-30 06:19:04 +08:00
|
|
|
The middleware will not override an "X-Content-Type-Options" header
|
2014-09-13 02:50:36 +08:00
|
|
|
already present in the response.
|
|
|
|
"""
|
2018-10-30 06:19:04 +08:00
|
|
|
response = self.process_response(
|
|
|
|
secure=True, headers={"X-Content-Type-Options": "foo"}
|
|
|
|
)
|
2020-07-14 19:32:24 +08:00
|
|
|
self.assertEqual(response.headers["X-Content-Type-Options"], "foo")
|
2014-09-13 02:50:36 +08:00
|
|
|
|
|
|
|
@override_settings(SECURE_CONTENT_TYPE_NOSNIFF=False)
|
|
|
|
def test_content_type_off(self):
|
|
|
|
"""
|
2019-03-21 08:15:34 +08:00
|
|
|
With SECURE_CONTENT_TYPE_NOSNIFF False, the middleware does not add an
|
2018-10-30 06:19:04 +08:00
|
|
|
"X-Content-Type-Options" header to the response.
|
2014-09-13 02:50:36 +08:00
|
|
|
"""
|
2020-07-14 19:32:24 +08:00
|
|
|
self.assertNotIn("X-Content-Type-Options", self.process_response().headers)
|
2014-09-13 02:50:36 +08:00
|
|
|
|
|
|
|
@override_settings(SECURE_SSL_REDIRECT=True)
|
|
|
|
def test_ssl_redirect_on(self):
|
|
|
|
"""
|
2019-03-21 08:15:34 +08:00
|
|
|
With SECURE_SSL_REDIRECT True, the middleware redirects any non-secure
|
2014-09-13 02:50:36 +08:00
|
|
|
requests to the https:// version of the same URL.
|
|
|
|
"""
|
|
|
|
ret = self.process_request("get", "/some/url?query=string")
|
|
|
|
self.assertEqual(ret.status_code, 301)
|
2019-03-21 08:15:34 +08:00
|
|
|
self.assertEqual(ret["Location"], "https://testserver/some/url?query=string")
|
2014-09-13 02:50:36 +08:00
|
|
|
|
|
|
|
@override_settings(SECURE_SSL_REDIRECT=True)
|
|
|
|
def test_no_redirect_ssl(self):
|
|
|
|
"""
|
|
|
|
The middleware does not redirect secure requests.
|
|
|
|
"""
|
|
|
|
ret = self.process_request("get", "/some/url", secure=True)
|
2016-06-17 02:19:18 +08:00
|
|
|
self.assertIsNone(ret)
|
2014-09-13 02:50:36 +08:00
|
|
|
|
2019-03-21 08:15:34 +08:00
|
|
|
@override_settings(SECURE_SSL_REDIRECT=True, SECURE_REDIRECT_EXEMPT=["^insecure/"])
|
2014-09-13 02:50:36 +08:00
|
|
|
def test_redirect_exempt(self):
|
|
|
|
"""
|
|
|
|
The middleware does not redirect requests with URL path matching an
|
|
|
|
exempt pattern.
|
|
|
|
"""
|
|
|
|
ret = self.process_request("get", "/insecure/page")
|
2016-06-17 02:19:18 +08:00
|
|
|
self.assertIsNone(ret)
|
2014-09-13 02:50:36 +08:00
|
|
|
|
2019-03-21 08:15:34 +08:00
|
|
|
@override_settings(SECURE_SSL_REDIRECT=True, SECURE_SSL_HOST="secure.example.com")
|
2014-09-13 02:50:36 +08:00
|
|
|
def test_redirect_ssl_host(self):
|
|
|
|
"""
|
2019-03-21 08:15:34 +08:00
|
|
|
The middleware redirects to SECURE_SSL_HOST if given.
|
2014-09-13 02:50:36 +08:00
|
|
|
"""
|
|
|
|
ret = self.process_request("get", "/some/url")
|
|
|
|
self.assertEqual(ret.status_code, 301)
|
|
|
|
self.assertEqual(ret["Location"], "https://secure.example.com/some/url")
|
|
|
|
|
|
|
|
@override_settings(SECURE_SSL_REDIRECT=False)
|
|
|
|
def test_ssl_redirect_off(self):
|
|
|
|
"""
|
2019-03-21 08:15:34 +08:00
|
|
|
With SECURE_SSL_REDIRECT False, the middleware does not redirect.
|
2014-09-13 02:50:36 +08:00
|
|
|
"""
|
|
|
|
ret = self.process_request("get", "/some/url")
|
2016-06-17 02:19:18 +08:00
|
|
|
self.assertIsNone(ret)
|
2019-03-22 05:33:41 +08:00
|
|
|
|
|
|
|
@override_settings(SECURE_REFERRER_POLICY=None)
|
|
|
|
def test_referrer_policy_off(self):
|
|
|
|
"""
|
|
|
|
With SECURE_REFERRER_POLICY set to None, the middleware does not add a
|
|
|
|
"Referrer-Policy" header to the response.
|
|
|
|
"""
|
2020-07-14 19:32:24 +08:00
|
|
|
self.assertNotIn("Referrer-Policy", self.process_response().headers)
|
2019-03-22 05:33:41 +08:00
|
|
|
|
|
|
|
def test_referrer_policy_on(self):
|
|
|
|
"""
|
|
|
|
With SECURE_REFERRER_POLICY set to a valid value, the middleware adds a
|
|
|
|
"Referrer-Policy" header to the response.
|
|
|
|
"""
|
|
|
|
tests = (
|
|
|
|
("strict-origin", "strict-origin"),
|
|
|
|
("strict-origin,origin", "strict-origin,origin"),
|
|
|
|
("strict-origin, origin", "strict-origin,origin"),
|
|
|
|
(["strict-origin", "origin"], "strict-origin,origin"),
|
|
|
|
(("strict-origin", "origin"), "strict-origin,origin"),
|
|
|
|
)
|
|
|
|
for value, expected in tests:
|
|
|
|
with self.subTest(value=value), override_settings(
|
|
|
|
SECURE_REFERRER_POLICY=value
|
|
|
|
):
|
2020-07-14 19:32:24 +08:00
|
|
|
self.assertEqual(
|
|
|
|
self.process_response().headers["Referrer-Policy"],
|
|
|
|
expected,
|
|
|
|
)
|
2019-03-22 05:33:41 +08:00
|
|
|
|
|
|
|
@override_settings(SECURE_REFERRER_POLICY="strict-origin")
|
|
|
|
def test_referrer_policy_already_present(self):
|
|
|
|
"""
|
|
|
|
The middleware will not override a "Referrer-Policy" header already
|
|
|
|
present in the response.
|
|
|
|
"""
|
|
|
|
response = self.process_response(headers={"Referrer-Policy": "unsafe-url"})
|
2020-07-14 19:32:24 +08:00
|
|
|
self.assertEqual(response.headers["Referrer-Policy"], "unsafe-url")
|
2020-08-27 00:09:19 +08:00
|
|
|
|
|
|
|
@override_settings(SECURE_CROSS_ORIGIN_OPENER_POLICY=None)
|
|
|
|
def test_coop_off(self):
|
|
|
|
"""
|
|
|
|
With SECURE_CROSS_ORIGIN_OPENER_POLICY set to None, the middleware does
|
|
|
|
not add a "Cross-Origin-Opener-Policy" header to the response.
|
|
|
|
"""
|
|
|
|
self.assertNotIn("Cross-Origin-Opener-Policy", self.process_response())
|
|
|
|
|
|
|
|
def test_coop_default(self):
|
|
|
|
"""SECURE_CROSS_ORIGIN_OPENER_POLICY defaults to same-origin."""
|
|
|
|
self.assertEqual(
|
|
|
|
self.process_response().headers["Cross-Origin-Opener-Policy"],
|
|
|
|
"same-origin",
|
|
|
|
)
|
|
|
|
|
|
|
|
def test_coop_on(self):
|
|
|
|
"""
|
|
|
|
With SECURE_CROSS_ORIGIN_OPENER_POLICY set to a valid value, the
|
|
|
|
middleware adds a "Cross-Origin_Opener-Policy" header to the response.
|
|
|
|
"""
|
|
|
|
tests = ["same-origin", "same-origin-allow-popups", "unsafe-none"]
|
|
|
|
for value in tests:
|
|
|
|
with self.subTest(value=value), override_settings(
|
|
|
|
SECURE_CROSS_ORIGIN_OPENER_POLICY=value,
|
|
|
|
):
|
|
|
|
self.assertEqual(
|
|
|
|
self.process_response().headers["Cross-Origin-Opener-Policy"],
|
|
|
|
value,
|
|
|
|
)
|
|
|
|
|
|
|
|
@override_settings(SECURE_CROSS_ORIGIN_OPENER_POLICY="unsafe-none")
|
|
|
|
def test_coop_already_present(self):
|
|
|
|
"""
|
|
|
|
The middleware doesn't override a "Cross-Origin-Opener-Policy" header
|
|
|
|
already present in the response.
|
|
|
|
"""
|
|
|
|
response = self.process_response(
|
|
|
|
headers={"Cross-Origin-Opener-Policy": "same-origin"}
|
|
|
|
)
|
|
|
|
self.assertEqual(response.headers["Cross-Origin-Opener-Policy"], "same-origin")
|