diff --git a/django/core/checks/security/base.py b/django/core/checks/security/base.py index d96c318add2..8cf3d1d61e1 100644 --- a/django/core/checks/security/base.py +++ b/django/core/checks/security/base.py @@ -9,6 +9,7 @@ REFERRER_POLICY_VALUES = { 'strict-origin-when-cross-origin', 'unsafe-url', } +SECRET_KEY_INSECURE_PREFIX = 'django-insecure-' SECRET_KEY_MIN_LENGTH = 50 SECRET_KEY_MIN_UNIQUE_CHARACTERS = 5 @@ -68,12 +69,14 @@ W008 = Warning( ) W009 = Warning( - "Your SECRET_KEY has less than %(min_length)s characters or less than " - "%(min_unique_chars)s unique characters. Please generate a long and random " - "SECRET_KEY, otherwise many of Django's security-critical features will be " - "vulnerable to attack." % { + "Your SECRET_KEY has less than %(min_length)s characters, less than " + "%(min_unique_chars)s unique characters, or it's prefixed with " + "'%(insecure_prefix)s' indicating that it was generated automatically by " + "Django. Please generate a long and random SECRET_KEY, otherwise many of " + "Django's security-critical features will be vulnerable to attack." % { 'min_length': SECRET_KEY_MIN_LENGTH, 'min_unique_chars': SECRET_KEY_MIN_UNIQUE_CHARACTERS, + 'insecure_prefix': SECRET_KEY_INSECURE_PREFIX, }, id='security.W009', ) @@ -195,7 +198,8 @@ def check_secret_key(app_configs, **kwargs): else: passed_check = ( len(set(secret_key)) >= SECRET_KEY_MIN_UNIQUE_CHARACTERS and - len(secret_key) >= SECRET_KEY_MIN_LENGTH + len(secret_key) >= SECRET_KEY_MIN_LENGTH and + not secret_key.startswith(SECRET_KEY_INSECURE_PREFIX) ) return [] if passed_check else [W009] diff --git a/django/core/management/commands/startproject.py b/django/core/management/commands/startproject.py index 7e09a25e914..164ccdffb52 100644 --- a/django/core/management/commands/startproject.py +++ b/django/core/management/commands/startproject.py @@ -1,3 +1,4 @@ +from django.core.checks.security.base import SECRET_KEY_INSECURE_PREFIX from django.core.management.templates import TemplateCommand from ..utils import get_random_secret_key @@ -15,6 +16,6 @@ class Command(TemplateCommand): target = options.pop('directory') # Create a random SECRET_KEY to put it in the main settings. - options['secret_key'] = get_random_secret_key() + options['secret_key'] = SECRET_KEY_INSECURE_PREFIX + get_random_secret_key() super().handle('project', project_name, target, **options) diff --git a/docs/ref/checks.txt b/docs/ref/checks.txt index b7f941ff83c..2ac5d4cb1d8 100644 --- a/docs/ref/checks.txt +++ b/docs/ref/checks.txt @@ -437,10 +437,11 @@ The following checks are run if you use the :option:`check --deploy` option: ``True``. Unless your site should be available over both SSL and non-SSL connections, you may want to either set this setting to ``True`` or configure a load balancer or reverse-proxy server to redirect all connections to HTTPS. -* **security.W009**: Your :setting:`SECRET_KEY` has less than 50 characters or - less than 5 unique characters. Please generate a long and random - ``SECRET_KEY``, otherwise many of Django's security-critical features will be - vulnerable to attack. +* **security.W009**: Your :setting:`SECRET_KEY` has less than 50 characters, + less than 5 unique characters, or it's prefixed with ``'django-insecure-'`` + indicating that it was generated automatically by Django. Please generate a + long and random ``SECRET_KEY``, otherwise many of Django's security-critical + features will be vulnerable to attack. * **security.W010**: You have :mod:`django.contrib.sessions` in your :setting:`INSTALLED_APPS` but you have not set :setting:`SESSION_COOKIE_SECURE` to ``True``. Using a secure-only session diff --git a/tests/check_framework/test_security.py b/tests/check_framework/test_security.py index 270fece6592..8225b99995a 100644 --- a/tests/check_framework/test_security.py +++ b/tests/check_framework/test_security.py @@ -1,5 +1,6 @@ from django.conf import settings from django.core.checks.security import base, csrf, sessions +from django.core.management.utils import get_random_secret_key from django.test import SimpleTestCase from django.test.utils import override_settings @@ -394,6 +395,12 @@ class CheckSecretKeyTest(SimpleTestCase): def test_none_secret_key(self): self.assertEqual(base.check_secret_key(None), [base.W009]) + @override_settings( + SECRET_KEY=base.SECRET_KEY_INSECURE_PREFIX + get_random_secret_key() + ) + def test_insecure_secret_key(self): + self.assertEqual(base.check_secret_key(None), [base.W009]) + @override_settings(SECRET_KEY=('abcdefghijklmnopqrstuvwx' * 2) + 'a') def test_low_length_secret_key(self): self.assertEqual(len(settings.SECRET_KEY), base.SECRET_KEY_MIN_LENGTH - 1)