diff --git a/django/core/checks/security/base.py b/django/core/checks/security/base.py index eee7f854c8d..f482f771532 100644 --- a/django/core/checks/security/base.py +++ b/django/core/checks/security/base.py @@ -101,6 +101,12 @@ W020 = Warning( id='security.W020', ) +W021 = Warning( + "You have not set the SECURE_HSTS_PRELOAD setting to True. Without this, " + "your site cannot be submitted to the browser preload list.", + id='security.W021', +) + def _security_middleware(): return ("django.middleware.security.SecurityMiddleware" in settings.MIDDLEWARE_CLASSES or @@ -140,6 +146,16 @@ def check_sts_include_subdomains(app_configs, **kwargs): return [] if passed_check else [W005] +@register(Tags.security, deploy=True) +def check_sts_preload(app_configs, **kwargs): + passed_check = ( + not _security_middleware() or + not settings.SECURE_HSTS_SECONDS or + settings.SECURE_HSTS_PRELOAD is True + ) + return [] if passed_check else [W021] + + @register(Tags.security, deploy=True) def check_content_type_nosniff(app_configs, **kwargs): passed_check = ( diff --git a/docs/ref/checks.txt b/docs/ref/checks.txt index 5eed9f0aede..e0c24eb07d8 100644 --- a/docs/ref/checks.txt +++ b/docs/ref/checks.txt @@ -596,6 +596,9 @@ The following checks are run if you use the :option:`check --deploy` option: for your site to serve other parts of itself in a frame, you should change it to ``'DENY'``. * **security.W020**: :setting:`ALLOWED_HOSTS` must not be empty in deployment. +* **security.W021**: You have not set the + :setting:`SECURE_HSTS_PRELOAD` setting to ``True``. Without this, your site + cannot be submitted to the browser preload list. Sites ----- diff --git a/docs/spelling_wordlist b/docs/spelling_wordlist index 5a935d30e14..7712c4c02e2 100644 --- a/docs/spelling_wordlist +++ b/docs/spelling_wordlist @@ -594,6 +594,7 @@ prefetches prefetching prefork preforked +preload prepend prepended prepending diff --git a/tests/check_framework/test_security.py b/tests/check_framework/test_security.py index b4591030f5b..ebd1ffb0d3a 100644 --- a/tests/check_framework/test_security.py +++ b/tests/check_framework/test_security.py @@ -307,6 +307,50 @@ class CheckStrictTransportSecuritySubdomainsTest(SimpleTestCase): self.assertEqual(self.func(None), []) +class CheckStrictTransportSecurityPreloadTest(SimpleTestCase): + @property + def func(self): + from django.core.checks.security.base import check_sts_preload + return check_sts_preload + + @override_settings( + MIDDLEWARE=["django.middleware.security.SecurityMiddleware"], + SECURE_HSTS_PRELOAD=False, + SECURE_HSTS_SECONDS=3600, + ) + def test_no_sts_preload(self): + """ + Warn if SECURE_HSTS_PRELOAD isn't True. + """ + self.assertEqual(self.func(None), [base.W021]) + + @override_settings(MIDDLEWARE=[], SECURE_HSTS_PRELOAD=False, SECURE_HSTS_SECONDS=3600) + def test_no_sts_preload_no_middleware(self): + """ + Don't warn if SecurityMiddleware isn't installed. + """ + self.assertEqual(self.func(None), []) + + @override_settings( + MIDDLEWARE=["django.middleware.security.SecurityMiddleware"], + SECURE_SSL_REDIRECT=False, + SECURE_HSTS_SECONDS=None, + ) + def test_no_sts_preload_no_seconds(self): + """ + Don't warn if SECURE_HSTS_SECONDS isn't set. + """ + self.assertEqual(self.func(None), []) + + @override_settings( + MIDDLEWARE=["django.middleware.security.SecurityMiddleware"], + SECURE_HSTS_PRELOAD=True, + SECURE_HSTS_SECONDS=3600, + ) + def test_with_sts_preload(self): + self.assertEqual(self.func(None), []) + + class CheckXFrameOptionsMiddlewareTest(SimpleTestCase): @property def func(self):