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:
Tim Graham 2014-09-12 14:50:36 -04:00
parent 8f334e55be
commit 52ef6a4726
24 changed files with 1638 additions and 22 deletions

View File

@ -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

View File

@ -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'

View File

@ -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',

View File

@ -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()

View File

View 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]

View 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]

View 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

View File

@ -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

View File

@ -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,
)

View 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

View File

@ -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

View File

@ -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
=================

View File

@ -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
=====================================

View File

@ -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
-----

View File

@ -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
---------------

View File

@ -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
------------------

View File

@ -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`

View File

@ -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
~~~~~~~~~~~~~~

View File

@ -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

View File

@ -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

View 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), [])

View File

@ -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 [

View 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)