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:
parent
8f334e55be
commit
52ef6a4726
|
@ -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,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]
|
|
@ -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]
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 [
|
||||
|
|
|
@ -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)
|
Loading…
Reference in New Issue