from django.http import HttpResponse from django.test import RequestFactory, SimpleTestCase from django.test.utils import override_settings class SecurityMiddlewareTest(SimpleTestCase): def middleware(self, *args, **kwargs): from django.middleware.security import SecurityMiddleware return SecurityMiddleware(self.response(*args, **kwargs)) @property def secure_request_kwargs(self): return {"wsgi.url_scheme": "https"} def response(self, *args, headers=None, **kwargs): def get_response(req): response = HttpResponse(*args, **kwargs) if headers: for k, v in headers.items(): response.headers[k] = v return response return get_response def process_response(self, *args, secure=False, request=None, **kwargs): request_kwargs = {} if secure: request_kwargs.update(self.secure_request_kwargs) if request is None: request = self.request.get("/some/url", **request_kwargs) ret = self.middleware(*args, **kwargs).process_request(request) if ret: return ret return self.middleware(*args, **kwargs)(request) request = RequestFactory() def process_request(self, method, *args, secure=False, **kwargs): if secure: kwargs.update(self.secure_request_kwargs) req = getattr(self.request, method.lower())(*args, **kwargs) return self.middleware().process_request(req) @override_settings(SECURE_HSTS_SECONDS=3600) def test_sts_on(self): """ With SECURE_HSTS_SECONDS=3600, the middleware adds "Strict-Transport-Security: max-age=3600" to the response. """ self.assertEqual( self.process_response(secure=True).headers['Strict-Transport-Security'], 'max-age=3600', ) @override_settings(SECURE_HSTS_SECONDS=3600) def test_sts_already_present(self): """ The middleware will not override a "Strict-Transport-Security" header already present in the response. """ response = self.process_response( secure=True, headers={"Strict-Transport-Security": "max-age=7200"}) self.assertEqual(response.headers["Strict-Transport-Security"], "max-age=7200") @override_settings(SECURE_HSTS_SECONDS=3600) def test_sts_only_if_secure(self): """ The "Strict-Transport-Security" header is not added to responses going over an insecure connection. """ self.assertNotIn( 'Strict-Transport-Security', self.process_response(secure=False).headers, ) @override_settings(SECURE_HSTS_SECONDS=0) def test_sts_off(self): """ With SECURE_HSTS_SECONDS=0, the middleware does not add a "Strict-Transport-Security" header to the response. """ self.assertNotIn( 'Strict-Transport-Security', self.process_response(secure=True).headers, ) @override_settings(SECURE_HSTS_SECONDS=600, SECURE_HSTS_INCLUDE_SUBDOMAINS=True) def test_sts_include_subdomains(self): """ With SECURE_HSTS_SECONDS non-zero and SECURE_HSTS_INCLUDE_SUBDOMAINS True, the middleware adds a "Strict-Transport-Security" header with the "includeSubDomains" directive to the response. """ response = self.process_response(secure=True) self.assertEqual( response.headers['Strict-Transport-Security'], 'max-age=600; includeSubDomains', ) @override_settings(SECURE_HSTS_SECONDS=600, SECURE_HSTS_INCLUDE_SUBDOMAINS=False) def test_sts_no_include_subdomains(self): """ With SECURE_HSTS_SECONDS non-zero and SECURE_HSTS_INCLUDE_SUBDOMAINS False, the middleware adds a "Strict-Transport-Security" header without the "includeSubDomains" directive to the response. """ response = self.process_response(secure=True) self.assertEqual(response.headers["Strict-Transport-Security"], "max-age=600") @override_settings(SECURE_HSTS_SECONDS=10886400, SECURE_HSTS_PRELOAD=True) def test_sts_preload(self): """ 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. """ response = self.process_response(secure=True) self.assertEqual( response.headers['Strict-Transport-Security'], 'max-age=10886400; preload', ) @override_settings(SECURE_HSTS_SECONDS=10886400, SECURE_HSTS_INCLUDE_SUBDOMAINS=True, SECURE_HSTS_PRELOAD=True) def test_sts_subdomains_and_preload(self): """ With SECURE_HSTS_SECONDS non-zero, SECURE_HSTS_INCLUDE_SUBDOMAINS and SECURE_HSTS_PRELOAD True, the middleware adds a "Strict-Transport-Security" header containing both the "includeSubDomains" and "preload" directives to the response. """ response = self.process_response(secure=True) self.assertEqual( response.headers['Strict-Transport-Security'], 'max-age=10886400; includeSubDomains; preload', ) @override_settings(SECURE_HSTS_SECONDS=10886400, SECURE_HSTS_PRELOAD=False) def test_sts_no_preload(self): """ With SECURE_HSTS_SECONDS non-zero and SECURE_HSTS_PRELOAD False, the middleware adds a "Strict-Transport-Security" header without the "preload" directive to the response. """ response = self.process_response(secure=True) self.assertEqual( response.headers['Strict-Transport-Security'], 'max-age=10886400', ) @override_settings(SECURE_CONTENT_TYPE_NOSNIFF=True) def test_content_type_on(self): """ With SECURE_CONTENT_TYPE_NOSNIFF set to True, the middleware adds "X-Content-Type-Options: nosniff" header to the response. """ self.assertEqual( self.process_response().headers['X-Content-Type-Options'], 'nosniff', ) @override_settings(SECURE_CONTENT_TYPE_NOSNIFF=True) def test_content_type_already_present(self): """ The middleware will not override an "X-Content-Type-Options" header already present in the response. """ response = self.process_response(secure=True, headers={"X-Content-Type-Options": "foo"}) self.assertEqual(response.headers["X-Content-Type-Options"], "foo") @override_settings(SECURE_CONTENT_TYPE_NOSNIFF=False) def test_content_type_off(self): """ With SECURE_CONTENT_TYPE_NOSNIFF False, the middleware does not add an "X-Content-Type-Options" header to the response. """ self.assertNotIn('X-Content-Type-Options', self.process_response().headers) @override_settings(SECURE_SSL_REDIRECT=True) def test_ssl_redirect_on(self): """ With SECURE_SSL_REDIRECT True, the middleware redirects any non-secure requests to the https:// version of the same URL. """ ret = self.process_request("get", "/some/url?query=string") self.assertEqual(ret.status_code, 301) self.assertEqual(ret["Location"], "https://testserver/some/url?query=string") @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) self.assertIsNone(ret) @override_settings(SECURE_SSL_REDIRECT=True, SECURE_REDIRECT_EXEMPT=["^insecure/"]) 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") self.assertIsNone(ret) @override_settings(SECURE_SSL_REDIRECT=True, SECURE_SSL_HOST="secure.example.com") def test_redirect_ssl_host(self): """ The middleware redirects to SECURE_SSL_HOST if given. """ 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): """ With SECURE_SSL_REDIRECT False, the middleware does not redirect. """ ret = self.process_request("get", "/some/url") self.assertIsNone(ret) @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. """ self.assertNotIn('Referrer-Policy', self.process_response().headers) 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): self.assertEqual( self.process_response().headers['Referrer-Policy'], expected, ) @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'}) self.assertEqual(response.headers['Referrer-Policy'], 'unsafe-url') @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')