from django.conf import settings from django.core.exceptions import ImproperlyConfigured from .. import Error, Tags, Warning, register CROSS_ORIGIN_OPENER_POLICY_VALUES = { "same-origin", "same-origin-allow-popups", "unsafe-none", } REFERRER_POLICY_VALUES = { "no-referrer", "no-referrer-when-downgrade", "origin", "origin-when-cross-origin", "same-origin", "strict-origin", "strict-origin-when-cross-origin", "unsafe-url", } SECRET_KEY_INSECURE_PREFIX = "django-insecure-" SECRET_KEY_MIN_LENGTH = 50 SECRET_KEY_MIN_UNIQUE_CHARACTERS = 5 SECRET_KEY_WARNING_MSG = ( f"Your %s has less than {SECRET_KEY_MIN_LENGTH} characters, less than " f"{SECRET_KEY_MIN_UNIQUE_CHARACTERS} unique characters, or it's prefixed " f"with '{SECRET_KEY_INSECURE_PREFIX}' indicating that it was generated " f"automatically by Django. Please generate a long and random value, " f"otherwise many of Django's security-critical features will be " f"vulnerable to attack." ) W001 = Warning( "You do not have 'django.middleware.security.SecurityMiddleware' " "in your MIDDLEWARE so the SECURE_HSTS_SECONDS, " "SECURE_CONTENT_TYPE_NOSNIFF, SECURE_REFERRER_POLICY, " "SECURE_CROSS_ORIGIN_OPENER_POLICY, and SECURE_SSL_REDIRECT settings will " "have no effect.", id="security.W001", ) W002 = Warning( "You do not have " "'django.middleware.clickjacking.XFrameOptionsMiddleware' in your " "MIDDLEWARE, so your pages will not be served with an " "'x-frame-options' header. Unless there is a good reason for your " "site to be served in a frame, you should consider enabling this " "header to help prevent clickjacking attacks.", id="security.W002", ) W004 = Warning( "You have not set a value for the SECURE_HSTS_SECONDS setting. " "If your entire site is served only over SSL, you may want to consider " "setting a value and enabling HTTP Strict Transport Security. " "Be sure to read the documentation first; enabling HSTS carelessly " "can cause serious, irreversible problems.", id="security.W004", ) W005 = Warning( "You have not set the SECURE_HSTS_INCLUDE_SUBDOMAINS setting to True. " "Without this, your site is potentially vulnerable to attack " "via an insecure connection to a subdomain. Only set this to True if " "you are certain that all subdomains of your domain should be served " "exclusively via SSL.", id="security.W005", ) W006 = Warning( "Your SECURE_CONTENT_TYPE_NOSNIFF setting is not set to True, " "so your pages will not be served with an " "'X-Content-Type-Options: nosniff' header. " "You should consider enabling this header to prevent the " "browser from identifying content types incorrectly.", id="security.W006", ) W008 = Warning( "Your SECURE_SSL_REDIRECT setting is not set to True. " "Unless your site should be available over both SSL and non-SSL " "connections, you may want to either set this setting True " "or configure a load balancer or reverse-proxy server " "to redirect all connections to HTTPS.", id="security.W008", ) W009 = Warning( SECRET_KEY_WARNING_MSG % "SECRET_KEY", id="security.W009", ) W018 = Warning( "You should not have DEBUG set to True in deployment.", id="security.W018", ) W019 = Warning( "You have " "'django.middleware.clickjacking.XFrameOptionsMiddleware' in your " "MIDDLEWARE, but X_FRAME_OPTIONS is not set to 'DENY'. " "Unless there is a good reason for your site to serve other parts of " "itself in a frame, you should change it to 'DENY'.", id="security.W019", ) W020 = Warning( "ALLOWED_HOSTS must not be empty in deployment.", 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", ) W022 = Warning( "You have not set the SECURE_REFERRER_POLICY setting. Without this, your " "site will not send a Referrer-Policy header. You should consider " "enabling this header to protect user privacy.", id="security.W022", ) E023 = Error( "You have set the SECURE_REFERRER_POLICY setting to an invalid value.", hint="Valid values are: {}.".format(", ".join(sorted(REFERRER_POLICY_VALUES))), id="security.E023", ) E024 = Error( "You have set the SECURE_CROSS_ORIGIN_OPENER_POLICY setting to an invalid " "value.", hint="Valid values are: {}.".format( ", ".join(sorted(CROSS_ORIGIN_OPENER_POLICY_VALUES)), ), id="security.E024", ) W025 = Warning(SECRET_KEY_WARNING_MSG, id="security.W025") def _security_middleware(): return "django.middleware.security.SecurityMiddleware" in settings.MIDDLEWARE def _xframe_middleware(): return ( "django.middleware.clickjacking.XFrameOptionsMiddleware" in settings.MIDDLEWARE ) @register(Tags.security, deploy=True) def check_security_middleware(app_configs, **kwargs): passed_check = _security_middleware() return [] if passed_check else [W001] @register(Tags.security, deploy=True) def check_xframe_options_middleware(app_configs, **kwargs): passed_check = _xframe_middleware() return [] if passed_check else [W002] @register(Tags.security, deploy=True) def check_sts(app_configs, **kwargs): passed_check = not _security_middleware() or settings.SECURE_HSTS_SECONDS return [] if passed_check else [W004] @register(Tags.security, deploy=True) def check_sts_include_subdomains(app_configs, **kwargs): passed_check = ( not _security_middleware() or not settings.SECURE_HSTS_SECONDS or settings.SECURE_HSTS_INCLUDE_SUBDOMAINS is True ) 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 = ( not _security_middleware() or settings.SECURE_CONTENT_TYPE_NOSNIFF is True ) return [] if passed_check else [W006] @register(Tags.security, deploy=True) def check_ssl_redirect(app_configs, **kwargs): passed_check = not _security_middleware() or settings.SECURE_SSL_REDIRECT is True return [] if passed_check else [W008] def _check_secret_key(secret_key): return ( len(set(secret_key)) >= SECRET_KEY_MIN_UNIQUE_CHARACTERS and len(secret_key) >= SECRET_KEY_MIN_LENGTH and not secret_key.startswith(SECRET_KEY_INSECURE_PREFIX) ) @register(Tags.security, deploy=True) def check_secret_key(app_configs, **kwargs): try: secret_key = settings.SECRET_KEY except (ImproperlyConfigured, AttributeError): passed_check = False else: passed_check = _check_secret_key(secret_key) return [] if passed_check else [W009] @register(Tags.security, deploy=True) def check_secret_key_fallbacks(app_configs, **kwargs): warnings = [] try: fallbacks = settings.SECRET_KEY_FALLBACKS except (ImproperlyConfigured, AttributeError): warnings.append(Warning(W025.msg % "SECRET_KEY_FALLBACKS", id=W025.id)) else: for index, key in enumerate(fallbacks): if not _check_secret_key(key): warnings.append( Warning(W025.msg % f"SECRET_KEY_FALLBACKS[{index}]", id=W025.id) ) return warnings @register(Tags.security, deploy=True) def check_debug(app_configs, **kwargs): passed_check = not settings.DEBUG return [] if passed_check else [W018] @register(Tags.security, deploy=True) def check_xframe_deny(app_configs, **kwargs): passed_check = not _xframe_middleware() or settings.X_FRAME_OPTIONS == "DENY" return [] if passed_check else [W019] @register(Tags.security, deploy=True) def check_allowed_hosts(app_configs, **kwargs): return [] if settings.ALLOWED_HOSTS else [W020] @register(Tags.security, deploy=True) def check_referrer_policy(app_configs, **kwargs): if _security_middleware(): if settings.SECURE_REFERRER_POLICY is None: return [W022] # Support a comma-separated string or iterable of values to allow fallback. if isinstance(settings.SECURE_REFERRER_POLICY, str): values = {v.strip() for v in settings.SECURE_REFERRER_POLICY.split(",")} else: values = set(settings.SECURE_REFERRER_POLICY) if not values <= REFERRER_POLICY_VALUES: return [E023] return [] @register(Tags.security, deploy=True) def check_cross_origin_opener_policy(app_configs, **kwargs): if ( _security_middleware() and settings.SECURE_CROSS_ORIGIN_OPENER_POLICY is not None and settings.SECURE_CROSS_ORIGIN_OPENER_POLICY not in CROSS_ORIGIN_OPENER_POLICY_VALUES ): return [E024] return []