mirror of
				https://github.com/django/django.git
				synced 2025-10-31 09:41:08 +00:00 
			
		
		
		
	Fixed #17101 -- Integrated django-secure and added check --deploy option
Thanks Carl Meyer for django-secure and for reviewing. Thanks also to Zach Borboa, Erik Romijn, Collin Anderson, and Jorge Carleitao for reviews.
This commit is contained in:
		| @@ -631,3 +631,14 @@ MIGRATION_MODULES = {} | ||||
| # serious issues like errors and criticals does not result in hiding the | ||||
| # message, but Django will not stop you from e.g. running server. | ||||
| SILENCED_SYSTEM_CHECKS = [] | ||||
|  | ||||
| ####################### | ||||
| # SECURITY MIDDLEWARE # | ||||
| ####################### | ||||
| SECURE_BROWSER_XSS_FILTER = False | ||||
| SECURE_CONTENT_TYPE_NOSNIFF = False | ||||
| SECURE_HSTS_INCLUDE_SUBDOMAINS = False | ||||
| SECURE_HSTS_SECONDS = 0 | ||||
| SECURE_REDIRECT_EXEMPT = [] | ||||
| SECURE_SSL_HOST = None | ||||
| SECURE_SSL_REDIRECT = False | ||||
|   | ||||
| @@ -46,6 +46,7 @@ MIDDLEWARE_CLASSES = ( | ||||
|     'django.contrib.auth.middleware.SessionAuthenticationMiddleware', | ||||
|     'django.contrib.messages.middleware.MessageMiddleware', | ||||
|     'django.middleware.clickjacking.XFrameOptionsMiddleware', | ||||
|     'django.middleware.security.SecurityMiddleware', | ||||
| ) | ||||
|  | ||||
| ROOT_URLCONF = '{{ project_name }}.urls' | ||||
|   | ||||
| @@ -10,6 +10,9 @@ from .registry import register, run_checks, tag_exists, Tags | ||||
| import django.core.checks.compatibility.django_1_6_0  # NOQA | ||||
| import django.core.checks.compatibility.django_1_7_0  # NOQA | ||||
| import django.core.checks.model_checks  # NOQA | ||||
| import django.core.checks.security.base  # NOQA | ||||
| import django.core.checks.security.csrf  # NOQA | ||||
| import django.core.checks.security.sessions  # NOQA | ||||
|  | ||||
| __all__ = [ | ||||
|     'CheckMessage', | ||||
|   | ||||
| @@ -13,6 +13,7 @@ class Tags(object): | ||||
|     admin = 'admin' | ||||
|     compatibility = 'compatibility' | ||||
|     models = 'models' | ||||
|     security = 'security' | ||||
|     signals = 'signals' | ||||
|  | ||||
|  | ||||
| @@ -20,8 +21,9 @@ class CheckRegistry(object): | ||||
|  | ||||
|     def __init__(self): | ||||
|         self.registered_checks = [] | ||||
|         self.deployment_checks = [] | ||||
|  | ||||
|     def register(self, *tags): | ||||
|     def register(self, *tags, **kwargs): | ||||
|         """ | ||||
|         Decorator. Register given function `f` labeled with given `tags`. The | ||||
|         function should receive **kwargs and return list of Errors and | ||||
| @@ -36,24 +38,28 @@ class CheckRegistry(object): | ||||
|                 return errors | ||||
|  | ||||
|         """ | ||||
|         kwargs.setdefault('deploy', False) | ||||
|  | ||||
|         def inner(check): | ||||
|             check.tags = tags | ||||
|             if check not in self.registered_checks: | ||||
|             if kwargs['deploy']: | ||||
|                 if check not in self.deployment_checks: | ||||
|                     self.deployment_checks.append(check) | ||||
|             elif check not in self.registered_checks: | ||||
|                 self.registered_checks.append(check) | ||||
|             return check | ||||
|  | ||||
|         return inner | ||||
|  | ||||
|     def run_checks(self, app_configs=None, tags=None): | ||||
|     def run_checks(self, app_configs=None, tags=None, include_deployment_checks=False): | ||||
|         """ Run all registered checks and return list of Errors and Warnings. | ||||
|         """ | ||||
|         errors = [] | ||||
|         checks = self.get_checks(include_deployment_checks) | ||||
|  | ||||
|         if tags is not None: | ||||
|             checks = [check for check in self.registered_checks | ||||
|             checks = [check for check in checks | ||||
|                       if hasattr(check, 'tags') and set(check.tags) & set(tags)] | ||||
|         else: | ||||
|             checks = self.registered_checks | ||||
|  | ||||
|         for check in checks: | ||||
|             new_errors = check(app_configs=app_configs) | ||||
| @@ -63,11 +69,17 @@ class CheckRegistry(object): | ||||
|             errors.extend(new_errors) | ||||
|         return errors | ||||
|  | ||||
|     def tag_exists(self, tag): | ||||
|         return tag in self.tags_available() | ||||
|     def tag_exists(self, tag, include_deployment_checks=False): | ||||
|         return tag in self.tags_available(include_deployment_checks) | ||||
|  | ||||
|     def tags_available(self): | ||||
|         return set(chain(*[check.tags for check in self.registered_checks if hasattr(check, 'tags')])) | ||||
|     def tags_available(self, deployment_checks=False): | ||||
|         return set(chain(*[check.tags for check in self.get_checks(deployment_checks) if hasattr(check, 'tags')])) | ||||
|  | ||||
|     def get_checks(self, include_deployment_checks=False): | ||||
|         checks = list(self.registered_checks) | ||||
|         if include_deployment_checks: | ||||
|             checks.extend(self.deployment_checks) | ||||
|         return checks | ||||
|  | ||||
|  | ||||
| registry = CheckRegistry() | ||||
|   | ||||
							
								
								
									
										0
									
								
								django/core/checks/security/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								django/core/checks/security/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										185
									
								
								django/core/checks/security/base.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										185
									
								
								django/core/checks/security/base.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,185 @@ | ||||
| from django.conf import settings | ||||
|  | ||||
| from .. import register, Tags, Warning | ||||
|  | ||||
|  | ||||
| SECRET_KEY_MIN_LENGTH = 50 | ||||
| SECRET_KEY_MIN_UNIQUE_CHARACTERS = 5 | ||||
|  | ||||
| W001 = Warning( | ||||
|     "You do not have 'django.middleware.security.SecurityMiddleware' " | ||||
|     "in your MIDDLEWARE_CLASSES so the SECURE_HSTS_SECONDS, " | ||||
|     "SECURE_CONTENT_TYPE_NOSNIFF, " | ||||
|     "SECURE_BROWSER_XSS_FILTER, 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_CLASSES, 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', | ||||
| ) | ||||
|  | ||||
| W007 = Warning( | ||||
|     "Your SECURE_BROWSER_XSS_FILTER setting is not set to True, " | ||||
|     "so your pages will not be served with an " | ||||
|     "'x-xss-protection: 1; mode=block' header. " | ||||
|     "You should consider enabling this header to activate the " | ||||
|     "browser's XSS filtering and help prevent XSS attacks.", | ||||
|     id='security.W007', | ||||
| ) | ||||
|  | ||||
| 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( | ||||
|     "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." % { | ||||
|         'min_length': SECRET_KEY_MIN_LENGTH, | ||||
|         'min_unique_chars': SECRET_KEY_MIN_UNIQUE_CHARACTERS, | ||||
|     }, | ||||
|     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_CLASSES, but X_FRAME_OPTIONS is not set to 'DENY'. " | ||||
|     "The default is 'SAMEORIGIN', but 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', | ||||
| ) | ||||
|  | ||||
|  | ||||
| def _security_middleware(): | ||||
|     return "django.middleware.security.SecurityMiddleware" in settings.MIDDLEWARE_CLASSES | ||||
|  | ||||
|  | ||||
| def _xframe_middleware(): | ||||
|     return "django.middleware.clickjacking.XFrameOptionsMiddleware" in settings.MIDDLEWARE_CLASSES | ||||
|  | ||||
|  | ||||
| @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_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_xss_filter(app_configs, **kwargs): | ||||
|     passed_check = ( | ||||
|         not _security_middleware() or | ||||
|         settings.SECURE_BROWSER_XSS_FILTER is True | ||||
|     ) | ||||
|     return [] if passed_check else [W007] | ||||
|  | ||||
|  | ||||
| @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] | ||||
|  | ||||
|  | ||||
| @register(Tags.security, deploy=True) | ||||
| def check_secret_key(app_configs, **kwargs): | ||||
|     passed_check = ( | ||||
|         getattr(settings, 'SECRET_KEY', None) and | ||||
|         len(set(settings.SECRET_KEY)) >= SECRET_KEY_MIN_UNIQUE_CHARACTERS and | ||||
|         len(settings.SECRET_KEY) >= SECRET_KEY_MIN_LENGTH | ||||
|     ) | ||||
|     return [] if passed_check else [W009] | ||||
|  | ||||
|  | ||||
| @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] | ||||
							
								
								
									
										57
									
								
								django/core/checks/security/csrf.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								django/core/checks/security/csrf.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,57 @@ | ||||
| from django.conf import settings | ||||
|  | ||||
| from .. import register, Tags, Warning | ||||
|  | ||||
|  | ||||
| W003 = Warning( | ||||
|     "You don't appear to be using Django's built-in " | ||||
|     "cross-site request forgery protection via the middleware " | ||||
|     "('django.middleware.csrf.CsrfViewMiddleware' is not in your " | ||||
|     "MIDDLEWARE_CLASSES). Enabling the middleware is the safest approach " | ||||
|     "to ensure you don't leave any holes.", | ||||
|     id='security.W003', | ||||
| ) | ||||
|  | ||||
| W016 = Warning( | ||||
|     "You have 'django.middleware.csrf.CsrfViewMiddleware' in your " | ||||
|     "MIDDLEWARE_CLASSES, but you have not set CSRF_COOKIE_SECURE to True. " | ||||
|     "Using a secure-only CSRF cookie makes it more difficult for network " | ||||
|     "traffic sniffers to steal the CSRF token.", | ||||
|     id='security.W016', | ||||
| ) | ||||
|  | ||||
| W017 = Warning( | ||||
|     "You have 'django.middleware.csrf.CsrfViewMiddleware' in your " | ||||
|     "MIDDLEWARE_CLASSES, but you have not set CSRF_COOKIE_HTTPONLY to True. " | ||||
|     "Using an HttpOnly CSRF cookie makes it more difficult for cross-site " | ||||
|     "scripting attacks to steal the CSRF token.", | ||||
|     id='security.W017', | ||||
| ) | ||||
|  | ||||
|  | ||||
| def _csrf_middleware(): | ||||
|     return "django.middleware.csrf.CsrfViewMiddleware" in settings.MIDDLEWARE_CLASSES | ||||
|  | ||||
|  | ||||
| @register(Tags.security, deploy=True) | ||||
| def check_csrf_middleware(app_configs, **kwargs): | ||||
|     passed_check = _csrf_middleware() | ||||
|     return [] if passed_check else [W003] | ||||
|  | ||||
|  | ||||
| @register(Tags.security, deploy=True) | ||||
| def check_csrf_cookie_secure(app_configs, **kwargs): | ||||
|     passed_check = ( | ||||
|         not _csrf_middleware() or | ||||
|         settings.CSRF_COOKIE_SECURE | ||||
|     ) | ||||
|     return [] if passed_check else [W016] | ||||
|  | ||||
|  | ||||
| @register(Tags.security, deploy=True) | ||||
| def check_csrf_cookie_httponly(app_configs, **kwargs): | ||||
|     passed_check = ( | ||||
|         not _csrf_middleware() or | ||||
|         settings.CSRF_COOKIE_HTTPONLY | ||||
|     ) | ||||
|     return [] if passed_check else [W017] | ||||
							
								
								
									
										97
									
								
								django/core/checks/security/sessions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								django/core/checks/security/sessions.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,97 @@ | ||||
| from django.conf import settings | ||||
|  | ||||
| from .. import register, Tags, Warning | ||||
|  | ||||
|  | ||||
| def add_session_cookie_message(message): | ||||
|     return message + ( | ||||
|         " Using a secure-only session cookie makes it more difficult for " | ||||
|         "network traffic sniffers to hijack user sessions." | ||||
|     ) | ||||
|  | ||||
| W010 = Warning( | ||||
|     add_session_cookie_message( | ||||
|         "You have 'django.contrib.sessions' in your INSTALLED_APPS, " | ||||
|         "but you have not set SESSION_COOKIE_SECURE to True." | ||||
|     ), | ||||
|     id='security.W010', | ||||
| ) | ||||
|  | ||||
| W011 = Warning( | ||||
|     add_session_cookie_message( | ||||
|         "You have 'django.contrib.sessions.middleware.SessionMiddleware' " | ||||
|         "in your MIDDLEWARE_CLASSES, but you have not set " | ||||
|         "SESSION_COOKIE_SECURE to True." | ||||
|     ), | ||||
|     id='security.W011', | ||||
| ) | ||||
|  | ||||
| W012 = Warning( | ||||
|     add_session_cookie_message("SESSION_COOKIE_SECURE is not set to True."), | ||||
|     id='security.W012', | ||||
| ) | ||||
|  | ||||
|  | ||||
| def add_httponly_message(message): | ||||
|     return message + ( | ||||
|         " Using an HttpOnly session cookie makes it more difficult for " | ||||
|         "cross-site scripting attacks to hijack user sessions." | ||||
|     ) | ||||
|  | ||||
|  | ||||
| W013 = Warning( | ||||
|     add_httponly_message( | ||||
|         "You have 'django.contrib.sessions' in your INSTALLED_APPS, " | ||||
|         "but you have not set SESSION_COOKIE_HTTPONLY to True.", | ||||
|     ), | ||||
|     id='security.W013', | ||||
| ) | ||||
|  | ||||
| W014 = Warning( | ||||
|     add_httponly_message( | ||||
|         "You have 'django.contrib.sessions.middleware.SessionMiddleware' " | ||||
|         "in your MIDDLEWARE_CLASSES, but you have not set " | ||||
|         "SESSION_COOKIE_HTTPONLY to True." | ||||
|     ), | ||||
|     id='security.W014', | ||||
| ) | ||||
|  | ||||
| W015 = Warning( | ||||
|     add_httponly_message("SESSION_COOKIE_HTTPONLY is not set to True."), | ||||
|     id='security.W015', | ||||
| ) | ||||
|  | ||||
|  | ||||
| @register(Tags.security, deploy=True) | ||||
| def check_session_cookie_secure(app_configs, **kwargs): | ||||
|     errors = [] | ||||
|     if not settings.SESSION_COOKIE_SECURE: | ||||
|         if _session_app(): | ||||
|             errors.append(W010) | ||||
|         if _session_middleware(): | ||||
|             errors.append(W011) | ||||
|         if len(errors) > 1: | ||||
|             errors = [W012] | ||||
|     return errors | ||||
|  | ||||
|  | ||||
| @register(Tags.security, deploy=True) | ||||
| def check_session_cookie_httponly(app_configs, **kwargs): | ||||
|     errors = [] | ||||
|     if not settings.SESSION_COOKIE_HTTPONLY: | ||||
|         if _session_app(): | ||||
|             errors.append(W013) | ||||
|         if _session_middleware(): | ||||
|             errors.append(W014) | ||||
|         if len(errors) > 1: | ||||
|             errors = [W015] | ||||
|     return errors | ||||
|  | ||||
|  | ||||
| def _session_middleware(): | ||||
|     return ("django.contrib.sessions.middleware.SessionMiddleware" in | ||||
|             settings.MIDDLEWARE_CLASSES) | ||||
|  | ||||
|  | ||||
| def _session_app(): | ||||
|     return "django.contrib.sessions" in settings.INSTALLED_APPS | ||||
| @@ -442,14 +442,19 @@ class BaseCommand(object): | ||||
|  | ||||
|         return self.check(app_configs=app_configs, display_num_errors=display_num_errors) | ||||
|  | ||||
|     def check(self, app_configs=None, tags=None, display_num_errors=False): | ||||
|     def check(self, app_configs=None, tags=None, display_num_errors=False, | ||||
|               include_deployment_checks=False): | ||||
|         """ | ||||
|         Uses the system check framework to validate entire Django project. | ||||
|         Raises CommandError for any serious message (error or critical errors). | ||||
|         If there are only light messages (like warnings), they are printed to | ||||
|         stderr and no exception is raised. | ||||
|         """ | ||||
|         all_issues = checks.run_checks(app_configs=app_configs, tags=tags) | ||||
|         all_issues = checks.run_checks( | ||||
|             app_configs=app_configs, | ||||
|             tags=tags, | ||||
|             include_deployment_checks=include_deployment_checks, | ||||
|         ) | ||||
|  | ||||
|         msg = "" | ||||
|         visible_issue_count = 0  # excludes silenced warnings | ||||
|   | ||||
| @@ -18,10 +18,13 @@ class Command(BaseCommand): | ||||
|             help='Run only checks labeled with given tag.') | ||||
|         parser.add_argument('--list-tags', action='store_true', dest='list_tags', | ||||
|             help='List available tags.') | ||||
|         parser.add_argument('--deploy', action='store_true', dest='deploy', | ||||
|             help='Check deployment settings.') | ||||
|  | ||||
|     def handle(self, *app_labels, **options): | ||||
|         include_deployment_checks = options['deploy'] | ||||
|         if options.get('list_tags'): | ||||
|             self.stdout.write('\n'.join(sorted(registry.tags_available()))) | ||||
|             self.stdout.write('\n'.join(sorted(registry.tags_available(include_deployment_checks)))) | ||||
|             return | ||||
|  | ||||
|         if app_labels: | ||||
| @@ -30,8 +33,20 @@ class Command(BaseCommand): | ||||
|             app_configs = None | ||||
|  | ||||
|         tags = options.get('tags', None) | ||||
|         if tags and any(not checks.tag_exists(tag) for tag in tags): | ||||
|             invalid_tag = next(tag for tag in tags if not checks.tag_exists(tag)) | ||||
|             raise CommandError('There is no system check with the "%s" tag.' % invalid_tag) | ||||
|         if tags: | ||||
|             try: | ||||
|                 invalid_tag = next( | ||||
|                     tag for tag in tags if not checks.tag_exists(tag, include_deployment_checks) | ||||
|                 ) | ||||
|             except StopIteration: | ||||
|                 # no invalid tags | ||||
|                 pass | ||||
|             else: | ||||
|                 raise CommandError('There is no system check with the "%s" tag.' % invalid_tag) | ||||
|  | ||||
|         self.check(app_configs=app_configs, tags=tags, display_num_errors=True) | ||||
|         self.check( | ||||
|             app_configs=app_configs, | ||||
|             tags=tags, | ||||
|             display_num_errors=True, | ||||
|             include_deployment_checks=include_deployment_checks, | ||||
|         ) | ||||
|   | ||||
							
								
								
									
										43
									
								
								django/middleware/security.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								django/middleware/security.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,43 @@ | ||||
| import re | ||||
|  | ||||
| from django.conf import settings | ||||
| from django.http import HttpResponsePermanentRedirect | ||||
|  | ||||
|  | ||||
| class SecurityMiddleware(object): | ||||
|     def __init__(self): | ||||
|         self.sts_seconds = settings.SECURE_HSTS_SECONDS | ||||
|         self.sts_include_subdomains = settings.SECURE_HSTS_INCLUDE_SUBDOMAINS | ||||
|         self.content_type_nosniff = settings.SECURE_CONTENT_TYPE_NOSNIFF | ||||
|         self.xss_filter = settings.SECURE_BROWSER_XSS_FILTER | ||||
|         self.redirect = settings.SECURE_SSL_REDIRECT | ||||
|         self.redirect_host = settings.SECURE_SSL_HOST | ||||
|         self.redirect_exempt = [re.compile(r) for r in settings.SECURE_REDIRECT_EXEMPT] | ||||
|  | ||||
|     def process_request(self, request): | ||||
|         path = request.path.lstrip("/") | ||||
|         if (self.redirect and not request.is_secure() and | ||||
|                 not any(pattern.search(path) | ||||
|                         for pattern in self.redirect_exempt)): | ||||
|             host = self.redirect_host or request.get_host() | ||||
|             return HttpResponsePermanentRedirect( | ||||
|                 "https://%s%s" % (host, request.get_full_path()) | ||||
|             ) | ||||
|  | ||||
|     def process_response(self, request, response): | ||||
|         if (self.sts_seconds and request.is_secure() and | ||||
|                 'strict-transport-security' not in response): | ||||
|             sts_header = "max-age=%s" % self.sts_seconds | ||||
|  | ||||
|             if self.sts_include_subdomains: | ||||
|                 sts_header = sts_header + "; includeSubDomains" | ||||
|  | ||||
|             response["strict-transport-security"] = sts_header | ||||
|  | ||||
|         if self.content_type_nosniff and 'x-content-type-options' not in response: | ||||
|             response["x-content-type-options"] = "nosniff" | ||||
|  | ||||
|         if self.xss_filter and 'x-xss-protection' not in response: | ||||
|             response["x-xss-protection"] = "1; mode=block" | ||||
|  | ||||
|         return response | ||||
| @@ -359,7 +359,7 @@ class modify_settings(override_settings): | ||||
|         super(modify_settings, self).enable() | ||||
|  | ||||
|  | ||||
| def override_system_checks(new_checks): | ||||
| def override_system_checks(new_checks, deployment_checks=None): | ||||
|     """ Acts as a decorator. Overrides list of registered system checks. | ||||
|     Useful when you override `INSTALLED_APPS`, e.g. if you exclude `auth` app, | ||||
|     you also need to exclude its system checks. """ | ||||
| @@ -371,10 +371,14 @@ def override_system_checks(new_checks): | ||||
|         def inner(*args, **kwargs): | ||||
|             old_checks = registry.registered_checks | ||||
|             registry.registered_checks = new_checks | ||||
|             old_deployment_checks = registry.deployment_checks | ||||
|             if deployment_checks is not None: | ||||
|                 registry.deployment_checks = deployment_checks | ||||
|             try: | ||||
|                 return test_func(*args, **kwargs) | ||||
|             finally: | ||||
|                 registry.registered_checks = old_checks | ||||
|                 registry.deployment_checks = old_deployment_checks | ||||
|         return inner | ||||
|     return outer | ||||
|  | ||||
|   | ||||
| @@ -29,6 +29,14 @@ you're releasing the source code for your project, a common practice is to | ||||
| publish suitable settings for development, and to use a private settings | ||||
| module for production. | ||||
|  | ||||
| Run ``manage.py check --deploy`` | ||||
| ================================ | ||||
|  | ||||
| Some of the checks described below can be automated using the | ||||
| :djadminopt:`--deploy` option of the :djadmin:`check` command. Be sure to run it | ||||
| against your production settings file as described in the option's | ||||
| documentation. | ||||
|  | ||||
| Critical settings | ||||
| ================= | ||||
|  | ||||
|   | ||||
| @@ -229,6 +229,7 @@ applications and Django provides multiple protection tools and mechanisms: | ||||
| * :doc:`Clickjacking protection <ref/clickjacking>` | ||||
| * :doc:`Cross Site Request Forgery protection <ref/contrib/csrf>` | ||||
| * :doc:`Cryptographic signing <topics/signing>` | ||||
| * :ref:`Security Middleware <security-middleware>` | ||||
|  | ||||
| Internationalization and localization | ||||
| ===================================== | ||||
|   | ||||
| @@ -20,6 +20,7 @@ Django's system checks are organized using the following tags: | ||||
| * ``signals``: Checks on signal declarations and handler registrations. | ||||
| * ``admin``: Checks of any admin site declarations. | ||||
| * ``compatibility``: Flagging potential problems with version upgrades. | ||||
| * ``security``: Checks security related configuration. | ||||
|  | ||||
| Some checks may be registered with multiple tags. | ||||
|  | ||||
| @@ -346,6 +347,110 @@ The following checks are performed when a model contains a | ||||
| * **contenttypes.E004**: ``<field>`` is not a ``ForeignKey`` to | ||||
|   ``contenttypes.ContentType``. | ||||
|  | ||||
| Security | ||||
| -------- | ||||
|  | ||||
| The security checks do not make your site secure. They do not audit code, do | ||||
| intrusion detection, or do anything particularly complex. Rather, they help | ||||
| perform an automated, low-hanging-fruit checklist. They help you remember the | ||||
| simple things that improve your site's security. | ||||
|  | ||||
| Some of these checks may not be appropriate for your particular deployment | ||||
| configuration. For instance, if you do your HTTP to HTTPS redirection in a load | ||||
| balancer, it'd be irritating to be constantly warned about not having enabled | ||||
| :setting:`SECURE_SSL_REDIRECT`. Use :setting:`SILENCED_SYSTEM_CHECKS` to | ||||
| silence unneeded checks. | ||||
|  | ||||
| The following checks will be run if you use the :djadminopt:`--deploy` option | ||||
| of the :djadmin:`check` command: | ||||
|  | ||||
| * **security.W001**: You do not have | ||||
|   :class:`django.middleware.security.SecurityMiddleware` in your | ||||
|   :setting:`MIDDLEWARE_CLASSES` so the :setting:`SECURE_HSTS_SECONDS`, | ||||
|   :setting:`SECURE_CONTENT_TYPE_NOSNIFF`, :setting:`SECURE_BROWSER_XSS_FILTER`, | ||||
|   and :setting:`SECURE_SSL_REDIRECT` settings will have no effect. | ||||
| * **security.W002**: You do not have | ||||
|   :class:`django.middleware.clickjacking.XFrameOptionsMiddleware` in your | ||||
|   :setting:`MIDDLEWARE_CLASSES`, 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. | ||||
| * **security.W003**: You don't appear to be using Django's built-in cross-site | ||||
|   request forgery protection via the middleware | ||||
|   (:class:`django.middleware.csrf.CsrfViewMiddleware` is not in your | ||||
|   :setting:`MIDDLEWARE_CLASSES`). Enabling the middleware is the safest | ||||
|   approach to ensure you don't leave any holes. | ||||
| * **security.W004**: You have not set a value for the | ||||
|   :setting:`SECURE_HSTS_SECONDS` setting. If your entire site is served only | ||||
|   over SSL, you may want to consider setting a value and enabling :ref:`HTTP | ||||
|   Strict Transport Security <http-strict-transport-security>`. Be sure to read | ||||
|   the documentation first; enabling HSTS carelessly can cause serious, | ||||
|   irreversible problems. | ||||
| * **security.W005**: You have not set the | ||||
|   :setting:`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. | ||||
| * **security.W006**: Your :setting:`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. | ||||
| * **security.W007**: Your :setting:`SECURE_BROWSER_XSS_FILTER` setting is not | ||||
|   set to ``True``, so your pages will not be served with an | ||||
|   ``'x-xss-protection: 1; mode=block'`` header. You should consider enabling | ||||
|   this header to activate the browser's XSS filtering and help prevent XSS | ||||
|   attacks. | ||||
| * **security.W008**: Your :setting:`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 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.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 | ||||
|   cookie makes it more difficult for network traffic sniffers to hijack user | ||||
|   sessions. | ||||
| * **security.W011**: You have | ||||
|   :class:`django.contrib.sessions.middleware.SessionMiddleware` in your | ||||
|   :setting:`MIDDLEWARE_CLASSES`, but you have not set | ||||
|   :setting:`SESSION_COOKIE_SECURE` to ``True``. Using a secure-only session | ||||
|   cookie makes it more difficult for network traffic sniffers to hijack user | ||||
|   sessions. | ||||
| * **security.W012**: :setting:`SESSION_COOKIE_SECURE` is not set to ``True``. | ||||
|   Using a secure-only session cookie makes it more difficult for network traffic | ||||
|   sniffers to hijack user sessions. | ||||
| * **security.W013**: You have :mod:`django.contrib.sessions` in your | ||||
|   :setting:`INSTALLED_APPS`, but you have not set | ||||
|   :setting:`SESSION_COOKIE_HTTPONLY` to ``True``. Using an ``HttpOnly`` session | ||||
|   cookie makes it more difficult for cross-site scripting attacks to hijack user | ||||
|   sessions. | ||||
| * **security.W014**: You have | ||||
|   :class:`django.contrib.sessions.middleware.SessionMiddleware` in your | ||||
|   :setting:`MIDDLEWARE_CLASSES`, but you have not set | ||||
|   :setting:`SESSION_COOKIE_HTTPONLY` to ``True``. Using an ``HttpOnly`` session | ||||
|   cookie makes it more difficult for cross-site scripting attacks to hijack user | ||||
|   sessions. | ||||
| * **security.W015**: :setting:`SESSION_COOKIE_HTTPONLY` is not set to ``True``. | ||||
|   Using an ``HttpOnly`` session cookie makes it more difficult for cross-site | ||||
|   scripting attacks to hijack user sessions. | ||||
| * **security.W016**: :setting:`CSRF_COOKIE_SECURE` is not set to ``True``. | ||||
|   Using a secure-only CSRF cookie makes it more difficult for network traffic | ||||
|   sniffers to steal the CSRF token. | ||||
| * **security.W017**: :setting:`CSRF_COOKIE_HTTPONLY` is not set to ``True``. | ||||
|   Using an ``HttpOnly`` CSRF cookie makes it more difficult for cross-site | ||||
|   scripting attacks to steal the CSRF token. | ||||
| * **security.W018**: You should not have :setting:`DEBUG` set to ``True`` in | ||||
|   deployment. | ||||
| * **security.W019**: You have | ||||
|   :class:`django.middleware.clickjacking.XFrameOptionsMiddleware` in your | ||||
|   :setting:`MIDDLEWARE_CLASSES`, but :setting:`X_FRAME_OPTIONS` is not set to | ||||
|   ``'DENY'``. The default is ``'SAMEORIGIN'``, but unless there is a good reason | ||||
|   for your site to serve other parts of itself in a frame, you should change | ||||
|   it to ``'DENY'``. | ||||
|  | ||||
| Sites | ||||
| ----- | ||||
|  | ||||
|   | ||||
| @@ -135,6 +135,25 @@ to perform only security and compatibility checks, you would run:: | ||||
|  | ||||
| List all available tags. | ||||
|  | ||||
| .. django-admin-option:: --deploy | ||||
|  | ||||
| .. versionadded:: 1.8 | ||||
|  | ||||
| The ``--deploy`` option activates some additional checks that are only relevant | ||||
| in a deployment setting. | ||||
|  | ||||
| You can use this option in your local development environment, but since your | ||||
| local development settings module may not have many of your production settings, | ||||
| you will probably want to point the ``check`` command at a different settings | ||||
| module, either by setting the ``DJANGO_SETTINGS_MODULE`` environment variable, | ||||
| or by passing the ``--settings`` option:: | ||||
|  | ||||
|     python manage.py check --deploy --settings=production_settings | ||||
|  | ||||
| Or you could run it directly on a production or staging deployment to verify | ||||
| that the correct settings are in use (omitting ``--settings``). You could even | ||||
| make it part of your integration test suite. | ||||
|  | ||||
| compilemessages | ||||
| --------------- | ||||
|  | ||||
|   | ||||
| @@ -155,6 +155,178 @@ Message middleware | ||||
| Enables cookie- and session-based message support. See the | ||||
| :doc:`messages documentation </ref/contrib/messages>`. | ||||
|  | ||||
| .. _security-middleware: | ||||
|  | ||||
| Security middleware | ||||
| ------------------- | ||||
|  | ||||
| .. module:: django.middleware.security | ||||
|     :synopsis: Security middleware. | ||||
|  | ||||
| .. warning:: | ||||
|     If your deployment situation allows, it's usually a good idea to have your | ||||
|     front-end Web server perform the functionality provided by the | ||||
|     ``SecurityMiddleware``. That way, if there are requests that aren't served | ||||
|     by Django (such as static media or user-uploaded files), they will have | ||||
|     the same protections as requests to your Django application. | ||||
|  | ||||
| .. class:: SecurityMiddleware | ||||
|  | ||||
| .. versionadded:: 1.8 | ||||
|  | ||||
| The ``django.middleware.security.SecurityMiddleware`` provides several security | ||||
| enhancements to the request/response cycle. Each one can be independently | ||||
| enabled or disabled with a setting. | ||||
|  | ||||
| * :setting:`SECURE_BROWSER_XSS_FILTER` | ||||
| * :setting:`SECURE_CONTENT_TYPE_NOSNIFF` | ||||
| * :setting:`SECURE_HSTS_INCLUDE_SUBDOMAINS` | ||||
| * :setting:`SECURE_HSTS_SECONDS` | ||||
| * :setting:`SECURE_REDIRECT_EXEMPT` | ||||
| * :setting:`SECURE_SSL_HOST` | ||||
| * :setting:`SECURE_SSL_REDIRECT` | ||||
|  | ||||
| .. _http-strict-transport-security: | ||||
|  | ||||
| HTTP Strict Transport Security | ||||
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||||
|  | ||||
| For sites that should only be accessed over HTTPS, you can instruct modern | ||||
| browsers to refuse to connect to your domain name via an insecure connection | ||||
| (for a given period of time) by setting the `"Strict-Transport-Security" | ||||
| header`_. This reduces your exposure to some SSL-stripping man-in-the-middle | ||||
| (MITM) attacks. | ||||
|  | ||||
| ``SecurityMiddleware`` will set this header for you on all HTTPS responses if | ||||
| you set the :setting:`SECURE_HSTS_SECONDS` setting to a non-zero integer value. | ||||
|  | ||||
| When enabling HSTS, it's a good idea to first use a small value for testing, | ||||
| for example, :setting:`SECURE_HSTS_SECONDS = 3600<SECURE_HSTS_SECONDS>` for one | ||||
| hour. Each time a Web browser sees the HSTS header from your site, it will | ||||
| refuse to communicate non-securely (using HTTP) with your domain for the given | ||||
| period of time. Once you confirm that all assets are served securely on your | ||||
| site (i.e. HSTS didn't break anything), it's a good idea to increase this value | ||||
| so that infrequent visitors will be protected (31536000 seconds, i.e. 1 year, | ||||
| is common). | ||||
|  | ||||
| Additionally, if you set the :setting:`SECURE_HSTS_INCLUDE_SUBDOMAINS` setting | ||||
| to ``True``, ``SecurityMiddleware`` will add the ``includeSubDomains`` tag to | ||||
| the ``Strict-Transport-Security`` header. This is recommended (assuming all | ||||
| subdomains are served exclusively using HTTPS), otherwise your site may still | ||||
| be vulnerable via an insecure connection to a subdomain. | ||||
|  | ||||
| .. warning:: | ||||
|     The HSTS policy applies to your entire domain, not just the URL of the | ||||
|     response that you set the header on. Therefore, you should only use it if | ||||
|     your entire domain is served via HTTPS only. | ||||
|  | ||||
|     Browsers properly respecting the HSTS header will refuse to allow users to | ||||
|     bypass warnings and connect to a site with an expired, self-signed, or | ||||
|     otherwise invalid SSL certificate. If you use HSTS, make sure your | ||||
|     certificates are in good shape and stay that way! | ||||
|  | ||||
| .. note:: | ||||
|     If you are deployed behind a load-balancer or reverse-proxy server, and the | ||||
|     ``Strict-Transport-Security`` header is not being added to your responses, | ||||
|     it may be because Django doesn't realize that it's on a secure connection; | ||||
|     you may need to set the :setting:`SECURE_PROXY_SSL_HEADER` setting. | ||||
|  | ||||
| .. _"Strict-Transport-Security" header: http://en.wikipedia.org/wiki/Strict_Transport_Security | ||||
|  | ||||
| .. _x-content-type-options: | ||||
|  | ||||
| ``X-Content-Type-Options: nosniff`` | ||||
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||||
|  | ||||
| Some browsers will try to guess the content types of the assets that they | ||||
| fetch, overriding the ``Content-Type`` header. While this can help display | ||||
| sites with improperly configured servers, it can also pose a security | ||||
| risk. | ||||
|  | ||||
| If your site serves user-uploaded files, a malicious user could upload a | ||||
| specially-crafted file that would be interpreted as HTML or Javascript by | ||||
| the browser when you expected it to be something harmless. | ||||
|  | ||||
| To learn more about this header and how the browser treats it, you can | ||||
| read about it on the `IE Security Blog`_. | ||||
|  | ||||
| To prevent the browser from guessing the content type and force it to | ||||
| always use the type provided in the ``Content-Type`` header, you can pass | ||||
| the ``X-Content-Type-Options: nosniff`` header.  ``SecurityMiddleware`` will | ||||
| do this for all responses if the :setting:`SECURE_CONTENT_TYPE_NOSNIFF` setting | ||||
| is ``True``. | ||||
|  | ||||
| Note that in most deployment situations where Django isn't involved in serving | ||||
| user-uploaded files, this setting won't help you. For example, if your | ||||
| :setting:`MEDIA_URL` is served directly by your front-end Web server (nginx, | ||||
| Apache, etc.) then you'd want to set this header there. On the other hand, if | ||||
| you are using Django to do something like require authorization in order to | ||||
| download files and you cannot set the header using your Web server, this | ||||
| setting will be useful. | ||||
|  | ||||
| .. _IE Security Blog: http://blogs.msdn.com/b/ie/archive/2008/09/02/ie8-security-part-vi-beta-2-update.aspx | ||||
|  | ||||
| .. _x-xss-protection: | ||||
|  | ||||
| ``X-XSS-Protection: 1; mode=block`` | ||||
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||||
|  | ||||
| Some browsers have the ability to block content that appears to be an `XSS | ||||
| attack`_. They work by looking for Javascript content in the GET or POST | ||||
| parameters of a page. If the Javascript is replayed in the server's response, | ||||
| the page is blocked from rendering and an error page is shown instead. | ||||
|  | ||||
| The `X-XSS-Protection header`_ is used to control the operation of the | ||||
| XSS filter. | ||||
|  | ||||
| To enable the XSS filter in the browser, and force it to always block | ||||
| suspected XSS attacks, you can pass the ``X-XSS-Protection: 1; mode=block`` | ||||
| header. ``SecurityMiddleware`` will do this for all responses if the | ||||
| :setting:`SECURE_BROWSER_XSS_FILTER` setting is ``True``. | ||||
|  | ||||
| .. warning:: | ||||
|     The browser XSS filter is a useful defense measure, but must not be | ||||
|     relied upon exclusively. It cannot detect all XSS attacks and not all | ||||
|     browsers support the header. Ensure you are still :ref:`validating and | ||||
|     sanitizing <cross-site-scripting>` all input to prevent XSS attacks. | ||||
|  | ||||
| .. _XSS attack: http://en.wikipedia.org/wiki/Cross-site_scripting | ||||
| .. _X-XSS-Protection header: http://blogs.msdn.com/b/ie/archive/2008/07/02/ie8-security-part-iv-the-xss-filter.aspx | ||||
|  | ||||
| .. _ssl-redirect: | ||||
|  | ||||
| SSL Redirect | ||||
| ~~~~~~~~~~~~ | ||||
|  | ||||
| If your site offers both HTTP and HTTPS connections, most users will end up | ||||
| with an unsecured connection by default. For best security, you should redirect | ||||
| all HTTP connections to HTTPS. | ||||
|  | ||||
| If you set the :setting:`SECURE_SSL_REDIRECT` setting to True, | ||||
| ``SecurityMiddleware`` will permanently (HTTP 301) redirect all HTTP | ||||
| connections to HTTPS. | ||||
|  | ||||
| .. note:: | ||||
|  | ||||
|     For performance reasons, it's preferable to do these redirects outside of | ||||
|     Django, in a front-end load balancer or reverse-proxy server such as | ||||
|     `nginx`_. :setting:`SECURE_SSL_REDIRECT` is intended for the deployment | ||||
|     situations where this isn't an option. | ||||
|  | ||||
| If the :setting:`SECURE_SSL_HOST` setting has a value, all redirects will be | ||||
| sent to that host instead of the originally-requested host. | ||||
|  | ||||
| If there are a few pages on your site that should be available over HTTP, and | ||||
| not redirected to HTTPS, you can list regular expressions to match those URLs | ||||
| in the :setting:`SECURE_REDIRECT_EXEMPT` setting. | ||||
|  | ||||
| .. note:: | ||||
|     If you are deployed behind a load-balancer or reverse-proxy server and | ||||
|     Django can't seem to tell when a request actually is already secure, you | ||||
|     may need to set the :setting:`SECURE_PROXY_SSL_HEADER` setting. | ||||
|  | ||||
| .. _nginx: http://nginx.org | ||||
|  | ||||
| Session middleware | ||||
| ------------------ | ||||
|  | ||||
|   | ||||
| @@ -357,6 +357,12 @@ Default: ``False`` | ||||
|  | ||||
| Whether to use ``HttpOnly`` flag on the CSRF cookie. If this is set to | ||||
| ``True``, client-side JavaScript will not to be able to access the CSRF cookie. | ||||
|  | ||||
| This can help prevent malicious JavaScript from bypassing CSRF protection. If | ||||
| you enable this and need to send the value of the CSRF token with Ajax requests, | ||||
| your JavaScript will need to pull the value from a hidden CSRF token form input | ||||
| on the page instead of from the cookie. | ||||
|  | ||||
| See :setting:`SESSION_COOKIE_HTTPONLY` for details on ``HttpOnly``. | ||||
|  | ||||
| .. setting:: CSRF_COOKIE_NAME | ||||
| @@ -1902,6 +1908,67 @@ Django will refuse to start if :setting:`SECRET_KEY` is not set. | ||||
|     security protections, and can lead to privilege escalation and remote code | ||||
|     execution vulnerabilities. | ||||
|  | ||||
| .. setting:: SECURE_BROWSER_XSS_FILTER | ||||
|  | ||||
| SECURE_BROWSER_XSS_FILTER | ||||
| ------------------------- | ||||
|  | ||||
| .. versionadded:: 1.8 | ||||
|  | ||||
| Default: ``False`` | ||||
|  | ||||
| If ``True``, the :class:`~django.middleware.security.SecurityMiddleware` sets | ||||
| the :ref:`x-xss-protection` header on all responses that do not already have it. | ||||
|  | ||||
| .. setting:: SECURE_CONTENT_TYPE_NOSNIFF | ||||
|  | ||||
| SECURE_CONTENT_TYPE_NOSNIFF | ||||
| --------------------------- | ||||
|  | ||||
| .. versionadded:: 1.8 | ||||
|  | ||||
| Default: ``False`` | ||||
|  | ||||
| If ``True``, the :class:`~django.middleware.security.SecurityMiddleware` | ||||
| sets the :ref:`x-content-type-options` header on all responses that do not | ||||
| already have it. | ||||
|  | ||||
| .. setting:: SECURE_HSTS_INCLUDE_SUBDOMAINS | ||||
|  | ||||
| SECURE_HSTS_INCLUDE_SUBDOMAINS | ||||
| ------------------------------ | ||||
|  | ||||
| .. versionadded:: 1.8 | ||||
|  | ||||
| Default: ``False`` | ||||
|  | ||||
| If ``True``, the :class:`~django.middleware.security.SecurityMiddleware` adds | ||||
| the ``includeSubDomains`` tag to the :ref:`http-strict-transport-security` | ||||
| header. It has no effect unless :setting:`SECURE_HSTS_SECONDS` is set to a | ||||
| non-zero value. | ||||
|  | ||||
| .. warning:: | ||||
|     Setting this incorrectly can irreversibly (for some time) break your site. | ||||
|     Read the :ref:`http-strict-transport-security` documentation first. | ||||
|  | ||||
| .. setting:: SECURE_HSTS_SECONDS | ||||
|  | ||||
| SECURE_HSTS_SECONDS | ||||
| ------------------- | ||||
|  | ||||
| .. versionadded:: 1.8 | ||||
|  | ||||
| Default: ``0`` | ||||
|  | ||||
| If set to a non-zero integer value, the | ||||
| :class:`~django.middleware.security.SecurityMiddleware` sets the | ||||
| :ref:`http-strict-transport-security` header on all responses that do not | ||||
| already have it. | ||||
|  | ||||
| .. warning:: | ||||
|     Setting this incorrectly can irreversibly (for some time) break your site. | ||||
|     Read the :ref:`http-strict-transport-security` documentation first. | ||||
|  | ||||
| .. setting:: SECURE_PROXY_SSL_HEADER | ||||
|  | ||||
| SECURE_PROXY_SSL_HEADER | ||||
| @@ -1963,6 +2030,55 @@ available in ``request.META``.) | ||||
|     If any of those are not true, you should keep this setting set to ``None`` | ||||
|     and find another way of determining HTTPS, perhaps via custom middleware. | ||||
|  | ||||
| .. setting:: SECURE_REDIRECT_EXEMPT | ||||
|  | ||||
| SECURE_REDIRECT_EXEMPT | ||||
| ---------------------- | ||||
|  | ||||
| .. versionadded:: 1.8 | ||||
|  | ||||
| Default: ``[]`` | ||||
|  | ||||
| If a URL path matches a regular expression in this list, the request will not be | ||||
| redirected to HTTPS. If :setting:`SECURE_SSL_REDIRECT` is ``False``, this | ||||
| setting has no effect. | ||||
|  | ||||
| .. setting:: SECURE_SSL_HOST | ||||
|  | ||||
| SECURE_SSL_HOST | ||||
| --------------- | ||||
|  | ||||
| .. versionadded:: 1.8 | ||||
|  | ||||
| Default: ``None`` | ||||
|  | ||||
| If a string (e.g. ``secure.example.com``), all SSL redirects will be directed | ||||
| to this host rather than the originally-requested host | ||||
| (e.g. ``www.example.com``). If :setting:`SECURE_SSL_REDIRECT` is ``False``, this | ||||
| setting has no effect. | ||||
|  | ||||
| .. setting:: SECURE_SSL_REDIRECT | ||||
|  | ||||
| SECURE_SSL_REDIRECT | ||||
| ------------------- | ||||
|  | ||||
| .. versionadded:: 1.8 | ||||
|  | ||||
| Default: ``False``. | ||||
|  | ||||
| If ``True``, the :class:`~django.middleware.security.SecurityMiddleware` | ||||
| :ref:`redirects <ssl-redirect>` all non-HTTPS requests to HTTPS (except for | ||||
| those URLs matching a regular expression listed in | ||||
| :setting:`SECURE_REDIRECT_EXEMPT`). | ||||
|  | ||||
| .. note:: | ||||
|  | ||||
|    If turning this to ``True`` causes infinite redirects, it probably means | ||||
|    your site is running behind a proxy and can't tell which requests are secure | ||||
|    and which are not. Your proxy likely sets a header to indicate secure | ||||
|    requests; you can correct the problem by finding out what that header is and | ||||
|    configuring the :setting:`SECURE_PROXY_SSL_HEADER` setting accordingly. | ||||
|  | ||||
| .. setting:: SERIALIZATION_MODULES | ||||
|  | ||||
| SERIALIZATION_MODULES | ||||
| @@ -2642,6 +2758,11 @@ consistently by all browsers. However, when it is honored, it can be a | ||||
| useful way to mitigate the risk of client side script accessing the | ||||
| protected cookie data. | ||||
|  | ||||
| Turning it on makes it less trivial for an attacker to escalate a cross-site | ||||
| scripting vulnerability into full hijacking of a user's session. There's not | ||||
| much excuse for leaving this off, either: if your code depends on reading | ||||
| session cookies from Javascript, you're probably doing it wrong. | ||||
|  | ||||
| .. versionadded:: 1.7 | ||||
|  | ||||
| This setting also affects cookies set by :mod:`django.contrib.messages`. | ||||
| @@ -2683,6 +2804,13 @@ Whether to use a secure cookie for the session cookie. If this is set to | ||||
| ``True``, the cookie will be marked as "secure," which means browsers may | ||||
| ensure that the cookie is only sent under an HTTPS connection. | ||||
|  | ||||
| Since it's trivial for a packet sniffer (e.g. `Firesheep`_) to hijack a user's | ||||
| session if the session cookie is sent unencrypted, there's really no good | ||||
| excuse to leave this off. It will prevent you from using sessions on insecure | ||||
| requests and that's a good thing. | ||||
|  | ||||
| .. _Firesheep: http://codebutler.com/firesheep | ||||
|  | ||||
| .. versionadded:: 1.7 | ||||
|  | ||||
| This setting also affects cookies set by :mod:`django.contrib.messages`. | ||||
| @@ -3023,7 +3151,16 @@ HTTP | ||||
| * :setting:`FORCE_SCRIPT_NAME` | ||||
| * :setting:`INTERNAL_IPS` | ||||
| * :setting:`MIDDLEWARE_CLASSES` | ||||
| * :setting:`SECURE_PROXY_SSL_HEADER` | ||||
| * Security | ||||
|  | ||||
|   * :setting:`SECURE_BROWSER_XSS_FILTER` | ||||
|   * :setting:`SECURE_CONTENT_TYPE_NOSNIFF` | ||||
|   * :setting:`SECURE_HSTS_INCLUDE_SUBDOMAINS` | ||||
|   * :setting:`SECURE_HSTS_SECONDS` | ||||
|   * :setting:`SECURE_PROXY_SSL_HEADER` | ||||
|   * :setting:`SECURE_REDIRECT_EXEMPT` | ||||
|   * :setting:`SECURE_SSL_HOST` | ||||
|   * :setting:`SECURE_SSL_REDIRECT` | ||||
| * :setting:`SIGNING_BACKEND` | ||||
| * :setting:`USE_ETAGS` | ||||
| * :setting:`USE_X_FORWARDED_HOST` | ||||
|   | ||||
| @@ -23,7 +23,17 @@ Like Django 1.7, Django 1.8 requires Python 2.7 or above, though we | ||||
| What's new in Django 1.8 | ||||
| ======================== | ||||
|  | ||||
| ... | ||||
| Security enhancements | ||||
| ~~~~~~~~~~~~~~~~~~~~~ | ||||
|  | ||||
| Several features of the django-secure_ third-party library have been | ||||
| integrated into Django. :class:`django.middleware.security.SecurityMiddleware` | ||||
| provides several security enhancements to the request/response cycle. The new | ||||
| :djadminopt:`--deploy` option of the :djadmin:`check` command allows you to | ||||
| check your production settings file for ways to increase the security of your | ||||
| site. | ||||
|  | ||||
| .. _django-secure: https://pypi.python.org/pypi/django-secure | ||||
|  | ||||
| Minor features | ||||
| ~~~~~~~~~~~~~~ | ||||
|   | ||||
| @@ -203,6 +203,7 @@ filesizeformat | ||||
| filesystem | ||||
| filesystems | ||||
| findstatic | ||||
| Firesheep | ||||
| firstof | ||||
| fk | ||||
| flatpage | ||||
| @@ -572,6 +573,7 @@ sqlmigrate | ||||
| sqlsequencereset | ||||
| squashmigrations | ||||
| ssi | ||||
| SSL | ||||
| stacktrace | ||||
| startswith | ||||
| stateful | ||||
| @@ -742,6 +744,7 @@ www | ||||
| xe | ||||
| xgettext | ||||
| xref | ||||
| XSS | ||||
| xxxxx | ||||
| yesno | ||||
| Zope | ||||
|   | ||||
| @@ -132,13 +132,25 @@ check. Tagging checks is useful since it allows you to run only a certain | ||||
| group of checks. For example, to register a compatibility check, you would | ||||
| make the following call:: | ||||
|  | ||||
|     from django.core.checks import register | ||||
|     from django.core.checks import register, Tags | ||||
|  | ||||
|     @register('compatibility') | ||||
|     @register(Tags.compatibility) | ||||
|     def my_check(app_configs, **kwargs): | ||||
|         # ... perform compatibility checks and collect errors | ||||
|         return errors | ||||
|  | ||||
| .. versionadded:: 1.8 | ||||
|  | ||||
| You can register "deployment checks" that are only relevant to a production | ||||
| settings file like this:: | ||||
|  | ||||
|     @register(Tags.security, deploy=True) | ||||
|     def my_check(app_configs, **kwargs): | ||||
|         ... | ||||
|  | ||||
| These checks will only be run if the :djadminopt:`--deploy` option is passed to | ||||
| the :djadmin:`check` command. | ||||
|  | ||||
| .. _field-checking: | ||||
|  | ||||
| Field, Model, and Manager checks | ||||
|   | ||||
							
								
								
									
										487
									
								
								tests/check_framework/test_security.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										487
									
								
								tests/check_framework/test_security.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,487 @@ | ||||
| from django.conf import settings | ||||
| from django.test import TestCase | ||||
| from django.test.utils import override_settings | ||||
|  | ||||
| from django.core.checks.security import base | ||||
| from django.core.checks.security import csrf | ||||
| from django.core.checks.security import sessions | ||||
|  | ||||
|  | ||||
| class CheckSessionCookieSecureTest(TestCase): | ||||
|     @property | ||||
|     def func(self): | ||||
|         from django.core.checks.security.sessions import check_session_cookie_secure | ||||
|         return check_session_cookie_secure | ||||
|  | ||||
|     @override_settings( | ||||
|         SESSION_COOKIE_SECURE=False, | ||||
|         INSTALLED_APPS=["django.contrib.sessions"], | ||||
|         MIDDLEWARE_CLASSES=[]) | ||||
|     def test_session_cookie_secure_with_installed_app(self): | ||||
|         """ | ||||
|         Warn if SESSION_COOKIE_SECURE is off and "django.contrib.sessions" is | ||||
|         in INSTALLED_APPS. | ||||
|         """ | ||||
|         self.assertEqual(self.func(None), [sessions.W010]) | ||||
|  | ||||
|     @override_settings( | ||||
|         SESSION_COOKIE_SECURE=False, | ||||
|         INSTALLED_APPS=[], | ||||
|         MIDDLEWARE_CLASSES=["django.contrib.sessions.middleware.SessionMiddleware"]) | ||||
|     def test_session_cookie_secure_with_middleware(self): | ||||
|         """ | ||||
|         Warn if SESSION_COOKIE_SECURE is off and | ||||
|         "django.contrib.sessions.middleware.SessionMiddleware" is in | ||||
|         MIDDLEWARE_CLASSES. | ||||
|         """ | ||||
|         self.assertEqual(self.func(None), [sessions.W011]) | ||||
|  | ||||
|     @override_settings( | ||||
|         SESSION_COOKIE_SECURE=False, | ||||
|         INSTALLED_APPS=["django.contrib.sessions"], | ||||
|         MIDDLEWARE_CLASSES=["django.contrib.sessions.middleware.SessionMiddleware"]) | ||||
|     def test_session_cookie_secure_both(self): | ||||
|         """ | ||||
|         If SESSION_COOKIE_SECURE is off and we find both the session app and | ||||
|         the middleware, provide one common warning. | ||||
|         """ | ||||
|         self.assertEqual(self.func(None), [sessions.W012]) | ||||
|  | ||||
|     @override_settings( | ||||
|         SESSION_COOKIE_SECURE=True, | ||||
|         INSTALLED_APPS=["django.contrib.sessions"], | ||||
|         MIDDLEWARE_CLASSES=["django.contrib.sessions.middleware.SessionMiddleware"]) | ||||
|     def test_session_cookie_secure_true(self): | ||||
|         """ | ||||
|         If SESSION_COOKIE_SECURE is on, there's no warning about it. | ||||
|         """ | ||||
|         self.assertEqual(self.func(None), []) | ||||
|  | ||||
|  | ||||
| class CheckSessionCookieHttpOnlyTest(TestCase): | ||||
|     @property | ||||
|     def func(self): | ||||
|         from django.core.checks.security.sessions import check_session_cookie_httponly | ||||
|         return check_session_cookie_httponly | ||||
|  | ||||
|     @override_settings( | ||||
|         SESSION_COOKIE_HTTPONLY=False, | ||||
|         INSTALLED_APPS=["django.contrib.sessions"], | ||||
|         MIDDLEWARE_CLASSES=[]) | ||||
|     def test_session_cookie_httponly_with_installed_app(self): | ||||
|         """ | ||||
|         Warn if SESSION_COOKIE_HTTPONLY is off and "django.contrib.sessions" | ||||
|         is in INSTALLED_APPS. | ||||
|         """ | ||||
|         self.assertEqual(self.func(None), [sessions.W013]) | ||||
|  | ||||
|     @override_settings( | ||||
|         SESSION_COOKIE_HTTPONLY=False, | ||||
|         INSTALLED_APPS=[], | ||||
|         MIDDLEWARE_CLASSES=["django.contrib.sessions.middleware.SessionMiddleware"]) | ||||
|     def test_session_cookie_httponly_with_middleware(self): | ||||
|         """ | ||||
|         Warn if SESSION_COOKIE_HTTPONLY is off and | ||||
|         "django.contrib.sessions.middleware.SessionMiddleware" is in | ||||
|         MIDDLEWARE_CLASSES. | ||||
|         """ | ||||
|         self.assertEqual(self.func(None), [sessions.W014]) | ||||
|  | ||||
|     @override_settings( | ||||
|         SESSION_COOKIE_HTTPONLY=False, | ||||
|         INSTALLED_APPS=["django.contrib.sessions"], | ||||
|         MIDDLEWARE_CLASSES=["django.contrib.sessions.middleware.SessionMiddleware"]) | ||||
|     def test_session_cookie_httponly_both(self): | ||||
|         """ | ||||
|         If SESSION_COOKIE_HTTPONLY is off and we find both the session app and | ||||
|         the middleware, provide one common warning. | ||||
|         """ | ||||
|         self.assertEqual(self.func(None), [sessions.W015]) | ||||
|  | ||||
|     @override_settings( | ||||
|         SESSION_COOKIE_HTTPONLY=True, | ||||
|         INSTALLED_APPS=["django.contrib.sessions"], | ||||
|         MIDDLEWARE_CLASSES=["django.contrib.sessions.middleware.SessionMiddleware"]) | ||||
|     def test_session_cookie_httponly_true(self): | ||||
|         """ | ||||
|         If SESSION_COOKIE_HTTPONLY is on, there's no warning about it. | ||||
|         """ | ||||
|         self.assertEqual(self.func(None), []) | ||||
|  | ||||
|  | ||||
| class CheckCSRFMiddlewareTest(TestCase): | ||||
|     @property | ||||
|     def func(self): | ||||
|         from django.core.checks.security.csrf import check_csrf_middleware | ||||
|         return check_csrf_middleware | ||||
|  | ||||
|     @override_settings(MIDDLEWARE_CLASSES=[]) | ||||
|     def test_no_csrf_middleware(self): | ||||
|         """ | ||||
|         Warn if CsrfViewMiddleware isn't in MIDDLEWARE_CLASSES. | ||||
|         """ | ||||
|         self.assertEqual(self.func(None), [csrf.W003]) | ||||
|  | ||||
|     @override_settings( | ||||
|         MIDDLEWARE_CLASSES=["django.middleware.csrf.CsrfViewMiddleware"]) | ||||
|     def test_with_csrf_middleware(self): | ||||
|         self.assertEqual(self.func(None), []) | ||||
|  | ||||
|  | ||||
| class CheckCSRFCookieSecureTest(TestCase): | ||||
|     @property | ||||
|     def func(self): | ||||
|         from django.core.checks.security.csrf import check_csrf_cookie_secure | ||||
|         return check_csrf_cookie_secure | ||||
|  | ||||
|     @override_settings( | ||||
|         MIDDLEWARE_CLASSES=["django.middleware.csrf.CsrfViewMiddleware"], | ||||
|         CSRF_COOKIE_SECURE=False) | ||||
|     def test_with_csrf_cookie_secure_false(self): | ||||
|         """ | ||||
|         Warn if CsrfViewMiddleware is in MIDDLEWARE_CLASSES but | ||||
|         CSRF_COOKIE_SECURE isn't True. | ||||
|         """ | ||||
|         self.assertEqual(self.func(None), [csrf.W016]) | ||||
|  | ||||
|     @override_settings(MIDDLEWARE_CLASSES=[], CSRF_COOKIE_SECURE=False) | ||||
|     def test_with_csrf_cookie_secure_false_no_middleware(self): | ||||
|         """ | ||||
|         No warning if CsrfViewMiddleware isn't in MIDDLEWARE_CLASSES, even if | ||||
|         CSRF_COOKIE_SECURE is False. | ||||
|         """ | ||||
|         self.assertEqual(self.func(None), []) | ||||
|  | ||||
|     @override_settings( | ||||
|         MIDDLEWARE_CLASSES=["django.middleware.csrf.CsrfViewMiddleware"], | ||||
|         CSRF_COOKIE_SECURE=True) | ||||
|     def test_with_csrf_cookie_secure_true(self): | ||||
|         self.assertEqual(self.func(None), []) | ||||
|  | ||||
|  | ||||
| class CheckCSRFCookieHttpOnlyTest(TestCase): | ||||
|     @property | ||||
|     def func(self): | ||||
|         from django.core.checks.security.csrf import check_csrf_cookie_httponly | ||||
|         return check_csrf_cookie_httponly | ||||
|  | ||||
|     @override_settings( | ||||
|         MIDDLEWARE_CLASSES=["django.middleware.csrf.CsrfViewMiddleware"], | ||||
|         CSRF_COOKIE_HTTPONLY=False) | ||||
|     def test_with_csrf_cookie_httponly_false(self): | ||||
|         """ | ||||
|         Warn if CsrfViewMiddleware is in MIDDLEWARE_CLASSES but | ||||
|         CSRF_COOKIE_HTTPONLY isn't True. | ||||
|         """ | ||||
|         self.assertEqual(self.func(None), [csrf.W017]) | ||||
|  | ||||
|     @override_settings(MIDDLEWARE_CLASSES=[], CSRF_COOKIE_HTTPONLY=False) | ||||
|     def test_with_csrf_cookie_httponly_false_no_middleware(self): | ||||
|         """ | ||||
|         No warning if CsrfViewMiddleware isn't in MIDDLEWARE_CLASSES, even if | ||||
|         CSRF_COOKIE_HTTPONLY is False. | ||||
|         """ | ||||
|         self.assertEqual(self.func(None), []) | ||||
|  | ||||
|     @override_settings( | ||||
|         MIDDLEWARE_CLASSES=["django.middleware.csrf.CsrfViewMiddleware"], | ||||
|         CSRF_COOKIE_HTTPONLY=True) | ||||
|     def test_with_csrf_cookie_httponly_true(self): | ||||
|         self.assertEqual(self.func(None), []) | ||||
|  | ||||
|  | ||||
| class CheckSecurityMiddlewareTest(TestCase): | ||||
|     @property | ||||
|     def func(self): | ||||
|         from django.core.checks.security.base import check_security_middleware | ||||
|         return check_security_middleware | ||||
|  | ||||
|     @override_settings(MIDDLEWARE_CLASSES=[]) | ||||
|     def test_no_security_middleware(self): | ||||
|         """ | ||||
|         Warn if SecurityMiddleware isn't in MIDDLEWARE_CLASSES. | ||||
|         """ | ||||
|         self.assertEqual(self.func(None), [base.W001]) | ||||
|  | ||||
|     @override_settings( | ||||
|         MIDDLEWARE_CLASSES=["django.middleware.security.SecurityMiddleware"]) | ||||
|     def test_with_security_middleware(self): | ||||
|         self.assertEqual(self.func(None), []) | ||||
|  | ||||
|  | ||||
| class CheckStrictTransportSecurityTest(TestCase): | ||||
|     @property | ||||
|     def func(self): | ||||
|         from django.core.checks.security.base import check_sts | ||||
|         return check_sts | ||||
|  | ||||
|     @override_settings( | ||||
|         MIDDLEWARE_CLASSES=["django.middleware.security.SecurityMiddleware"], | ||||
|         SECURE_HSTS_SECONDS=0) | ||||
|     def test_no_sts(self): | ||||
|         """ | ||||
|         Warn if SECURE_HSTS_SECONDS isn't > 0. | ||||
|         """ | ||||
|         self.assertEqual(self.func(None), [base.W004]) | ||||
|  | ||||
|     @override_settings( | ||||
|         MIDDLEWARE_CLASSES=[], | ||||
|         SECURE_HSTS_SECONDS=0) | ||||
|     def test_no_sts_no_middlware(self): | ||||
|         """ | ||||
|         Don't warn if SECURE_HSTS_SECONDS isn't > 0 and SecurityMiddleware isn't | ||||
|         installed. | ||||
|         """ | ||||
|         self.assertEqual(self.func(None), []) | ||||
|  | ||||
|     @override_settings( | ||||
|         MIDDLEWARE_CLASSES=["django.middleware.security.SecurityMiddleware"], | ||||
|         SECURE_HSTS_SECONDS=3600) | ||||
|     def test_with_sts(self): | ||||
|         self.assertEqual(self.func(None), []) | ||||
|  | ||||
|  | ||||
| class CheckStrictTransportSecuritySubdomainsTest(TestCase): | ||||
|     @property | ||||
|     def func(self): | ||||
|         from django.core.checks.security.base import check_sts_include_subdomains | ||||
|         return check_sts_include_subdomains | ||||
|  | ||||
|     @override_settings( | ||||
|         MIDDLEWARE_CLASSES=["django.middleware.security.SecurityMiddleware"], | ||||
|         SECURE_HSTS_INCLUDE_SUBDOMAINS=False, | ||||
|         SECURE_HSTS_SECONDS=3600) | ||||
|     def test_no_sts_subdomains(self): | ||||
|         """ | ||||
|         Warn if SECURE_HSTS_INCLUDE_SUBDOMAINS isn't True. | ||||
|         """ | ||||
|         self.assertEqual(self.func(None), [base.W005]) | ||||
|  | ||||
|     @override_settings( | ||||
|         MIDDLEWARE_CLASSES=[], | ||||
|         SECURE_HSTS_INCLUDE_SUBDOMAINS=False, | ||||
|         SECURE_HSTS_SECONDS=3600) | ||||
|     def test_no_sts_subdomains_no_middlware(self): | ||||
|         """ | ||||
|         Don't warn if SecurityMiddleware isn't installed. | ||||
|         """ | ||||
|         self.assertEqual(self.func(None), []) | ||||
|  | ||||
|     @override_settings( | ||||
|         MIDDLEWARE_CLASSES=["django.middleware.security.SecurityMiddleware"], | ||||
|         SECURE_SSL_REDIRECT=False, | ||||
|         SECURE_HSTS_SECONDS=None) | ||||
|     def test_no_sts_subdomains_no_seconds(self): | ||||
|         """ | ||||
|         Don't warn if SECURE_HSTS_SECONDS isn't set. | ||||
|         """ | ||||
|         self.assertEqual(self.func(None), []) | ||||
|  | ||||
|     @override_settings( | ||||
|         MIDDLEWARE_CLASSES=["django.middleware.security.SecurityMiddleware"], | ||||
|         SECURE_HSTS_INCLUDE_SUBDOMAINS=True, | ||||
|         SECURE_HSTS_SECONDS=3600) | ||||
|     def test_with_sts_subdomains(self): | ||||
|         self.assertEqual(self.func(None), []) | ||||
|  | ||||
|  | ||||
| class CheckXFrameOptionsMiddlewareTest(TestCase): | ||||
|     @property | ||||
|     def func(self): | ||||
|         from django.core.checks.security.base import check_xframe_options_middleware | ||||
|         return check_xframe_options_middleware | ||||
|  | ||||
|     @override_settings(MIDDLEWARE_CLASSES=[]) | ||||
|     def test_middleware_not_installed(self): | ||||
|         """ | ||||
|         Warn if XFrameOptionsMiddleware isn't in MIDDLEWARE_CLASSES. | ||||
|         """ | ||||
|         self.assertEqual(self.func(None), [base.W002]) | ||||
|  | ||||
|     @override_settings(MIDDLEWARE_CLASSES=["django.middleware.clickjacking.XFrameOptionsMiddleware"]) | ||||
|     def test_middleware_installed(self): | ||||
|         self.assertEqual(self.func(None), []) | ||||
|  | ||||
|  | ||||
| class CheckXFrameOptionsDenyTest(TestCase): | ||||
|     @property | ||||
|     def func(self): | ||||
|         from django.core.checks.security.base import check_xframe_deny | ||||
|         return check_xframe_deny | ||||
|  | ||||
|     @override_settings( | ||||
|         MIDDLEWARE_CLASSES=["django.middleware.clickjacking.XFrameOptionsMiddleware"], | ||||
|         X_FRAME_OPTIONS='SAMEORIGIN', | ||||
|     ) | ||||
|     def test_x_frame_options_not_deny(self): | ||||
|         """ | ||||
|         Warn if XFrameOptionsMiddleware is in MIDDLEWARE_CLASSES but | ||||
|         X_FRAME_OPTIONS isn't 'DENY'. | ||||
|         """ | ||||
|         self.assertEqual(self.func(None), [base.W019]) | ||||
|  | ||||
|     @override_settings(MIDDLEWARE_CLASSES=[], X_FRAME_OPTIONS='SAMEORIGIN') | ||||
|     def test_middleware_not_installed(self): | ||||
|         """ | ||||
|         No error if XFrameOptionsMiddleware isn't in MIDDLEWARE_CLASSES even if | ||||
|         X_FRAME_OPTIONS isn't 'DENY'. | ||||
|         """ | ||||
|         self.assertEqual(self.func(None), []) | ||||
|  | ||||
|     @override_settings( | ||||
|         MIDDLEWARE_CLASSES=["django.middleware.clickjacking.XFrameOptionsMiddleware"], | ||||
|         X_FRAME_OPTIONS='DENY', | ||||
|     ) | ||||
|     def test_xframe_deny(self): | ||||
|         self.assertEqual(self.func(None), []) | ||||
|  | ||||
|  | ||||
| class CheckContentTypeNosniffTest(TestCase): | ||||
|     @property | ||||
|     def func(self): | ||||
|         from django.core.checks.security.base import check_content_type_nosniff | ||||
|         return check_content_type_nosniff | ||||
|  | ||||
|     @override_settings( | ||||
|         MIDDLEWARE_CLASSES=["django.middleware.security.SecurityMiddleware"], | ||||
|         SECURE_CONTENT_TYPE_NOSNIFF=False) | ||||
|     def test_no_content_type_nosniff(self): | ||||
|         """ | ||||
|         Warn if SECURE_CONTENT_TYPE_NOSNIFF isn't True. | ||||
|         """ | ||||
|         self.assertEqual(self.func(None), [base.W006]) | ||||
|  | ||||
|     @override_settings( | ||||
|         MIDDLEWARE_CLASSES=[], | ||||
|         SECURE_CONTENT_TYPE_NOSNIFF=False) | ||||
|     def test_no_content_type_nosniff_no_middleware(self): | ||||
|         """ | ||||
|         Don't warn if SECURE_CONTENT_TYPE_NOSNIFF isn't True and | ||||
|         SecurityMiddleware isn't in MIDDLEWARE_CLASSES. | ||||
|         """ | ||||
|         self.assertEqual(self.func(None), []) | ||||
|  | ||||
|     @override_settings( | ||||
|         MIDDLEWARE_CLASSES=["django.middleware.security.SecurityMiddleware"], | ||||
|         SECURE_CONTENT_TYPE_NOSNIFF=True) | ||||
|     def test_with_content_type_nosniff(self): | ||||
|         self.assertEqual(self.func(None), []) | ||||
|  | ||||
|  | ||||
| class CheckXssFilterTest(TestCase): | ||||
|     @property | ||||
|     def func(self): | ||||
|         from django.core.checks.security.base import check_xss_filter | ||||
|         return check_xss_filter | ||||
|  | ||||
|     @override_settings( | ||||
|         MIDDLEWARE_CLASSES=["django.middleware.security.SecurityMiddleware"], | ||||
|         SECURE_BROWSER_XSS_FILTER=False) | ||||
|     def test_no_xss_filter(self): | ||||
|         """ | ||||
|         Warn if SECURE_BROWSER_XSS_FILTER isn't True. | ||||
|         """ | ||||
|         self.assertEqual(self.func(None), [base.W007]) | ||||
|  | ||||
|     @override_settings( | ||||
|         MIDDLEWARE_CLASSES=[], | ||||
|         SECURE_BROWSER_XSS_FILTER=False) | ||||
|     def test_no_xss_filter_no_middleware(self): | ||||
|         """ | ||||
|         Don't warn if SECURE_BROWSER_XSS_FILTER isn't True and | ||||
|         SecurityMiddleware isn't in MIDDLEWARE_CLASSES. | ||||
|         """ | ||||
|         self.assertEqual(self.func(None), []) | ||||
|  | ||||
|     @override_settings( | ||||
|         MIDDLEWARE_CLASSES=["django.middleware.security.SecurityMiddleware"], | ||||
|         SECURE_BROWSER_XSS_FILTER=True) | ||||
|     def test_with_xss_filter(self): | ||||
|         self.assertEqual(self.func(None), []) | ||||
|  | ||||
|  | ||||
| class CheckSSLRedirectTest(TestCase): | ||||
|     @property | ||||
|     def func(self): | ||||
|         from django.core.checks.security.base import check_ssl_redirect | ||||
|         return check_ssl_redirect | ||||
|  | ||||
|     @override_settings( | ||||
|         MIDDLEWARE_CLASSES=["django.middleware.security.SecurityMiddleware"], | ||||
|         SECURE_SSL_REDIRECT=False) | ||||
|     def test_no_ssl_redirect(self): | ||||
|         """ | ||||
|         Warn if SECURE_SSL_REDIRECT isn't True. | ||||
|         """ | ||||
|         self.assertEqual(self.func(None), [base.W008]) | ||||
|  | ||||
|     @override_settings( | ||||
|         MIDDLEWARE_CLASSES=[], | ||||
|         SECURE_SSL_REDIRECT=False) | ||||
|     def test_no_ssl_redirect_no_middlware(self): | ||||
|         """ | ||||
|         Don't warn if SECURE_SSL_REDIRECT is False and SecurityMiddleware isn't | ||||
|         installed. | ||||
|         """ | ||||
|         self.assertEqual(self.func(None), []) | ||||
|  | ||||
|     @override_settings( | ||||
|         MIDDLEWARE_CLASSES=["django.middleware.security.SecurityMiddleware"], | ||||
|         SECURE_SSL_REDIRECT=True) | ||||
|     def test_with_ssl_redirect(self): | ||||
|         self.assertEqual(self.func(None), []) | ||||
|  | ||||
|  | ||||
| class CheckSecretKeyTest(TestCase): | ||||
|     @property | ||||
|     def func(self): | ||||
|         from django.core.checks.security.base import check_secret_key | ||||
|         return check_secret_key | ||||
|  | ||||
|     @override_settings(SECRET_KEY=('abcdefghijklmnopqrstuvwx' * 2) + 'ab') | ||||
|     def test_okay_secret_key(self): | ||||
|         self.assertEqual(len(settings.SECRET_KEY), base.SECRET_KEY_MIN_LENGTH) | ||||
|         self.assertGreater(len(set(settings.SECRET_KEY)), base.SECRET_KEY_MIN_UNIQUE_CHARACTERS) | ||||
|         self.assertEqual(self.func(None), []) | ||||
|  | ||||
|     @override_settings(SECRET_KEY='') | ||||
|     def test_empty_secret_key(self): | ||||
|         self.assertEqual(self.func(None), [base.W009]) | ||||
|  | ||||
|     @override_settings(SECRET_KEY=None) | ||||
|     def test_missing_secret_key(self): | ||||
|         del settings.SECRET_KEY | ||||
|         self.assertEqual(self.func(None), [base.W009]) | ||||
|  | ||||
|     @override_settings(SECRET_KEY=None) | ||||
|     def test_none_secret_key(self): | ||||
|         self.assertEqual(self.func(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) | ||||
|         self.assertEqual(self.func(None), [base.W009]) | ||||
|  | ||||
|     @override_settings(SECRET_KEY='abcd' * 20) | ||||
|     def test_low_entropy_secret_key(self): | ||||
|         self.assertGreater(len(settings.SECRET_KEY), base.SECRET_KEY_MIN_LENGTH) | ||||
|         self.assertLess(len(set(settings.SECRET_KEY)), base.SECRET_KEY_MIN_UNIQUE_CHARACTERS) | ||||
|         self.assertEqual(self.func(None), [base.W009]) | ||||
|  | ||||
|  | ||||
| class CheckDebugTest(TestCase): | ||||
|     @property | ||||
|     def func(self): | ||||
|         from django.core.checks.security.base import check_debug | ||||
|         return check_debug | ||||
|  | ||||
|     @override_settings(DEBUG=True) | ||||
|     def test_debug_true(self): | ||||
|         """ | ||||
|         Warn if DEBUG is True. | ||||
|         """ | ||||
|         self.assertEqual(self.func(None), [base.W018]) | ||||
|  | ||||
|     @override_settings(DEBUG=False) | ||||
|     def test_debug_false(self): | ||||
|         self.assertEqual(self.func(None), []) | ||||
| @@ -194,6 +194,12 @@ def tagged_system_check(**kwargs): | ||||
| tagged_system_check.tags = ['simpletag'] | ||||
|  | ||||
|  | ||||
| def deployment_system_check(**kwargs): | ||||
|     deployment_system_check.kwargs = kwargs | ||||
|     return [checks.Warning('Deployment Check')] | ||||
| deployment_system_check.tags = ['deploymenttag'] | ||||
|  | ||||
|  | ||||
| class CheckCommandTests(TestCase): | ||||
|  | ||||
|     def setUp(self): | ||||
| @@ -239,6 +245,27 @@ class CheckCommandTests(TestCase): | ||||
|         call_command('check', list_tags=True) | ||||
|         self.assertEqual('simpletag\n', sys.stdout.getvalue()) | ||||
|  | ||||
|     @override_system_checks([tagged_system_check], deployment_checks=[deployment_system_check]) | ||||
|     def test_list_deployment_check_omitted(self): | ||||
|         call_command('check', list_tags=True) | ||||
|         self.assertEqual('simpletag\n', sys.stdout.getvalue()) | ||||
|  | ||||
|     @override_system_checks([tagged_system_check], deployment_checks=[deployment_system_check]) | ||||
|     def test_list_deployment_check_included(self): | ||||
|         call_command('check', deploy=True, list_tags=True) | ||||
|         self.assertEqual('deploymenttag\nsimpletag\n', sys.stdout.getvalue()) | ||||
|  | ||||
|     @override_system_checks([tagged_system_check], deployment_checks=[deployment_system_check]) | ||||
|     def test_tags_deployment_check_omitted(self): | ||||
|         msg = 'There is no system check with the "deploymenttag" tag.' | ||||
|         with self.assertRaisesMessage(CommandError, msg): | ||||
|             call_command('check', tags=['deploymenttag']) | ||||
|  | ||||
|     @override_system_checks([tagged_system_check], deployment_checks=[deployment_system_check]) | ||||
|     def test_tags_deployment_check_included(self): | ||||
|         call_command('check', deploy=True, tags=['deploymenttag']) | ||||
|         self.assertIn('Deployment Check', sys.stderr.getvalue()) | ||||
|  | ||||
|  | ||||
| def custom_error_system_check(app_configs, **kwargs): | ||||
|     return [ | ||||
|   | ||||
							
								
								
									
										202
									
								
								tests/middleware/test_security.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										202
									
								
								tests/middleware/test_security.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,202 @@ | ||||
| from django.http import HttpResponse | ||||
| from django.test import TestCase, RequestFactory | ||||
| from django.test.utils import override_settings | ||||
|  | ||||
|  | ||||
| class SecurityMiddlewareTest(TestCase): | ||||
|     @property | ||||
|     def middleware(self): | ||||
|         from django.middleware.security import SecurityMiddleware | ||||
|         return SecurityMiddleware() | ||||
|  | ||||
|     @property | ||||
|     def secure_request_kwargs(self): | ||||
|         return {"wsgi.url_scheme": "https"} | ||||
|  | ||||
|     def response(self, *args, **kwargs): | ||||
|         headers = kwargs.pop("headers", {}) | ||||
|         response = HttpResponse(*args, **kwargs) | ||||
|         for k, v in headers.items(): | ||||
|             response[k] = v | ||||
|         return response | ||||
|  | ||||
|     def process_response(self, *args, **kwargs): | ||||
|         request_kwargs = {} | ||||
|         if kwargs.pop("secure", False): | ||||
|             request_kwargs.update(self.secure_request_kwargs) | ||||
|         request = (kwargs.pop("request", None) or | ||||
|                    self.request.get("/some/url", **request_kwargs)) | ||||
|         ret = self.middleware.process_request(request) | ||||
|         if ret: | ||||
|             return ret | ||||
|         return self.middleware.process_response( | ||||
|             request, self.response(*args, **kwargs)) | ||||
|  | ||||
|     request = RequestFactory() | ||||
|  | ||||
|     def process_request(self, method, *args, **kwargs): | ||||
|         if kwargs.pop("secure", False): | ||||
|             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 HSTS_SECONDS=3600, the middleware adds | ||||
|         "strict-transport-security: max-age=3600" to the response. | ||||
|         """ | ||||
|         self.assertEqual( | ||||
|             self.process_response(secure=True)["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["strict-transport-security"], "max-age=7200") | ||||
|  | ||||
|     @override_settings(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)) | ||||
|  | ||||
|     @override_settings(HSTS_SECONDS=0) | ||||
|     def test_sts_off(self): | ||||
|         """ | ||||
|         With HSTS_SECONDS of 0, the middleware does not add a | ||||
|         "strict-transport-security" header to the response. | ||||
|         """ | ||||
|         self.assertNotIn("strict-transport-security", self.process_response(secure=True)) | ||||
|  | ||||
|     @override_settings( | ||||
|         SECURE_HSTS_SECONDS=600, SECURE_HSTS_INCLUDE_SUBDOMAINS=True) | ||||
|     def test_sts_include_subdomains(self): | ||||
|         """ | ||||
|         With HSTS_SECONDS non-zero and HSTS_INCLUDE_SUBDOMAINS | ||||
|         True, the middleware adds a "strict-transport-security" header with the | ||||
|         "includeSubDomains" tag to the response. | ||||
|         """ | ||||
|         response = self.process_response(secure=True) | ||||
|         self.assertEqual( | ||||
|             response["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 HSTS_SECONDS non-zero and HSTS_INCLUDE_SUBDOMAINS | ||||
|         False, the middleware adds a "strict-transport-security" header without | ||||
|         the "includeSubDomains" tag to the response. | ||||
|         """ | ||||
|         response = self.process_response(secure=True) | ||||
|         self.assertEqual(response["strict-transport-security"], "max-age=600") | ||||
|  | ||||
|     @override_settings(SECURE_CONTENT_TYPE_NOSNIFF=True) | ||||
|     def test_content_type_on(self): | ||||
|         """ | ||||
|         With CONTENT_TYPE_NOSNIFF set to True, the middleware adds | ||||
|         "x-content-type-options: nosniff" header to the response. | ||||
|         """ | ||||
|         self.assertEqual(self.process_response()["x-content-type-options"], "nosniff") | ||||
|  | ||||
|     @override_settings(SECURE_CONTENT_TYPE_NO_SNIFF=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["x-content-type-options"], "foo") | ||||
|  | ||||
|     @override_settings(SECURE_CONTENT_TYPE_NOSNIFF=False) | ||||
|     def test_content_type_off(self): | ||||
|         """ | ||||
|         With 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()) | ||||
|  | ||||
|     @override_settings(SECURE_BROWSER_XSS_FILTER=True) | ||||
|     def test_xss_filter_on(self): | ||||
|         """ | ||||
|         With BROWSER_XSS_FILTER set to True, the middleware adds | ||||
|         "s-xss-protection: 1; mode=block" header to the response. | ||||
|         """ | ||||
|         self.assertEqual( | ||||
|             self.process_response()["x-xss-protection"], | ||||
|             "1; mode=block") | ||||
|  | ||||
|     @override_settings(SECURE_BROWSER_XSS_FILTER=True) | ||||
|     def test_xss_filter_already_present(self): | ||||
|         """ | ||||
|         The middleware will not override an "x-xss-protection" header | ||||
|         already present in the response. | ||||
|         """ | ||||
|         response = self.process_response(secure=True, headers={"x-xss-protection": "foo"}) | ||||
|         self.assertEqual(response["x-xss-protection"], "foo") | ||||
|  | ||||
|     @override_settings(BROWSER_XSS_FILTER=False) | ||||
|     def test_xss_filter_off(self): | ||||
|         """ | ||||
|         With BROWSER_XSS_FILTER set to False, the middleware does not add an | ||||
|         "x-xss-protection" header to the response. | ||||
|         """ | ||||
|         self.assertFalse("x-xss-protection" in self.process_response()) | ||||
|  | ||||
|     @override_settings(SECURE_SSL_REDIRECT=True) | ||||
|     def test_ssl_redirect_on(self): | ||||
|         """ | ||||
|         With 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.assertEqual(ret, None) | ||||
|  | ||||
|     @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.assertEqual(ret, None) | ||||
|  | ||||
|     @override_settings( | ||||
|         SECURE_SSL_REDIRECT=True, SECURE_SSL_HOST="secure.example.com") | ||||
|     def test_redirect_ssl_host(self): | ||||
|         """ | ||||
|         The middleware redirects to 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 SSL_REDIRECT False, the middleware does no redirect. | ||||
|         """ | ||||
|         ret = self.process_request("get", "/some/url") | ||||
|         self.assertEqual(ret, None) | ||||
		Reference in New Issue
	
	Block a user