Skip to content

Commit 52ef6a4

Browse files
committed
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.
1 parent 8f334e5 commit 52ef6a4

File tree

24 files changed

+1639
-23
lines changed

24 files changed

+1639
-23
lines changed

django/conf/global_settings.py

+11
Original file line numberDiff line numberDiff line change
@@ -631,3 +631,14 @@
631631
# serious issues like errors and criticals does not result in hiding the
632632
# message, but Django will not stop you from e.g. running server.
633633
SILENCED_SYSTEM_CHECKS = []
634+
635+
#######################
636+
# SECURITY MIDDLEWARE #
637+
#######################
638+
SECURE_BROWSER_XSS_FILTER = False
639+
SECURE_CONTENT_TYPE_NOSNIFF = False
640+
SECURE_HSTS_INCLUDE_SUBDOMAINS = False
641+
SECURE_HSTS_SECONDS = 0
642+
SECURE_REDIRECT_EXEMPT = []
643+
SECURE_SSL_HOST = None
644+
SECURE_SSL_REDIRECT = False

django/conf/project_template/project_name/settings.py

+1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
4747
'django.contrib.messages.middleware.MessageMiddleware',
4848
'django.middleware.clickjacking.XFrameOptionsMiddleware',
49+
'django.middleware.security.SecurityMiddleware',
4950
)
5051

5152
ROOT_URLCONF = '{{ project_name }}.urls'

django/core/checks/__init__.py

+3
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
import django.core.checks.compatibility.django_1_6_0 # NOQA
1111
import django.core.checks.compatibility.django_1_7_0 # NOQA
1212
import django.core.checks.model_checks # NOQA
13+
import django.core.checks.security.base # NOQA
14+
import django.core.checks.security.csrf # NOQA
15+
import django.core.checks.security.sessions # NOQA
1316

1417
__all__ = [
1518
'CheckMessage',

django/core/checks/registry.py

+22-10
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,17 @@ class Tags(object):
1313
admin = 'admin'
1414
compatibility = 'compatibility'
1515
models = 'models'
16+
security = 'security'
1617
signals = 'signals'
1718

1819

1920
class CheckRegistry(object):
2021

2122
def __init__(self):
2223
self.registered_checks = []
24+
self.deployment_checks = []
2325

24-
def register(self, *tags):
26+
def register(self, *tags, **kwargs):
2527
"""
2628
Decorator. Register given function `f` labeled with given `tags`. The
2729
function should receive **kwargs and return list of Errors and
@@ -36,24 +38,28 @@ def my_check(apps, **kwargs):
3638
return errors
3739
3840
"""
41+
kwargs.setdefault('deploy', False)
3942

4043
def inner(check):
4144
check.tags = tags
42-
if check not in self.registered_checks:
45+
if kwargs['deploy']:
46+
if check not in self.deployment_checks:
47+
self.deployment_checks.append(check)
48+
elif check not in self.registered_checks:
4349
self.registered_checks.append(check)
4450
return check
4551

4652
return inner
4753

48-
def run_checks(self, app_configs=None, tags=None):
54+
def run_checks(self, app_configs=None, tags=None, include_deployment_checks=False):
4955
""" Run all registered checks and return list of Errors and Warnings.
5056
"""
5157
errors = []
58+
checks = self.get_checks(include_deployment_checks)
59+
5260
if tags is not None:
53-
checks = [check for check in self.registered_checks
61+
checks = [check for check in checks
5462
if hasattr(check, 'tags') and set(check.tags) & set(tags)]
55-
else:
56-
checks = self.registered_checks
5763

5864
for check in checks:
5965
new_errors = check(app_configs=app_configs)
@@ -63,11 +69,17 @@ def run_checks(self, app_configs=None, tags=None):
6369
errors.extend(new_errors)
6470
return errors
6571

66-
def tag_exists(self, tag):
67-
return tag in self.tags_available()
72+
def tag_exists(self, tag, include_deployment_checks=False):
73+
return tag in self.tags_available(include_deployment_checks)
74+
75+
def tags_available(self, deployment_checks=False):
76+
return set(chain(*[check.tags for check in self.get_checks(deployment_checks) if hasattr(check, 'tags')]))
6877

69-
def tags_available(self):
70-
return set(chain(*[check.tags for check in self.registered_checks if hasattr(check, 'tags')]))
78+
def get_checks(self, include_deployment_checks=False):
79+
checks = list(self.registered_checks)
80+
if include_deployment_checks:
81+
checks.extend(self.deployment_checks)
82+
return checks
7183

7284

7385
registry = CheckRegistry()

django/core/checks/security/__init__.py

Whitespace-only changes.

django/core/checks/security/base.py

+185
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
from django.conf import settings
2+
3+
from .. import register, Tags, Warning
4+
5+
6+
SECRET_KEY_MIN_LENGTH = 50
7+
SECRET_KEY_MIN_UNIQUE_CHARACTERS = 5
8+
9+
W001 = Warning(
10+
"You do not have 'django.middleware.security.SecurityMiddleware' "
11+
"in your MIDDLEWARE_CLASSES so the SECURE_HSTS_SECONDS, "
12+
"SECURE_CONTENT_TYPE_NOSNIFF, "
13+
"SECURE_BROWSER_XSS_FILTER, and SECURE_SSL_REDIRECT settings "
14+
"will have no effect.",
15+
id='security.W001',
16+
)
17+
18+
W002 = Warning(
19+
"You do not have "
20+
"'django.middleware.clickjacking.XFrameOptionsMiddleware' in your "
21+
"MIDDLEWARE_CLASSES, so your pages will not be served with an "
22+
"'x-frame-options' header. Unless there is a good reason for your "
23+
"site to be served in a frame, you should consider enabling this "
24+
"header to help prevent clickjacking attacks.",
25+
id='security.W002',
26+
)
27+
28+
W004 = Warning(
29+
"You have not set a value for the SECURE_HSTS_SECONDS setting. "
30+
"If your entire site is served only over SSL, you may want to consider "
31+
"setting a value and enabling HTTP Strict Transport Security. "
32+
"Be sure to read the documentation first; enabling HSTS carelessly "
33+
"can cause serious, irreversible problems.",
34+
id='security.W004',
35+
)
36+
37+
W005 = Warning(
38+
"You have not set the SECURE_HSTS_INCLUDE_SUBDOMAINS setting to True. "
39+
"Without this, your site is potentially vulnerable to attack "
40+
"via an insecure connection to a subdomain. Only set this to True if "
41+
"you are certain that all subdomains of your domain should be served "
42+
"exclusively via SSL.",
43+
id='security.W005',
44+
)
45+
46+
W006 = Warning(
47+
"Your SECURE_CONTENT_TYPE_NOSNIFF setting is not set to True, "
48+
"so your pages will not be served with an "
49+
"'x-content-type-options: nosniff' header. "
50+
"You should consider enabling this header to prevent the "
51+
"browser from identifying content types incorrectly.",
52+
id='security.W006',
53+
)
54+
55+
W007 = Warning(
56+
"Your SECURE_BROWSER_XSS_FILTER setting is not set to True, "
57+
"so your pages will not be served with an "
58+
"'x-xss-protection: 1; mode=block' header. "
59+
"You should consider enabling this header to activate the "
60+
"browser's XSS filtering and help prevent XSS attacks.",
61+
id='security.W007',
62+
)
63+
64+
W008 = Warning(
65+
"Your SECURE_SSL_REDIRECT setting is not set to True. "
66+
"Unless your site should be available over both SSL and non-SSL "
67+
"connections, you may want to either set this setting True "
68+
"or configure a load balancer or reverse-proxy server "
69+
"to redirect all connections to HTTPS.",
70+
id='security.W008',
71+
)
72+
73+
W009 = Warning(
74+
"Your SECRET_KEY has less than %(min_length)s characters or less than "
75+
"%(min_unique_chars)s unique characters. Please generate a long and random "
76+
"SECRET_KEY, otherwise many of Django's security-critical features will be "
77+
"vulnerable to attack." % {
78+
'min_length': SECRET_KEY_MIN_LENGTH,
79+
'min_unique_chars': SECRET_KEY_MIN_UNIQUE_CHARACTERS,
80+
},
81+
id='security.W009',
82+
)
83+
84+
W018 = Warning(
85+
"You should not have DEBUG set to True in deployment.",
86+
id='security.W018',
87+
)
88+
89+
W019 = Warning(
90+
"You have "
91+
"'django.middleware.clickjacking.XFrameOptionsMiddleware' in your "
92+
"MIDDLEWARE_CLASSES, but X_FRAME_OPTIONS is not set to 'DENY'. "
93+
"The default is 'SAMEORIGIN', but unless there is a good reason for "
94+
"your site to serve other parts of itself in a frame, you should "
95+
"change it to 'DENY'.",
96+
id='security.W019',
97+
)
98+
99+
100+
def _security_middleware():
101+
return "django.middleware.security.SecurityMiddleware" in settings.MIDDLEWARE_CLASSES
102+
103+
104+
def _xframe_middleware():
105+
return "django.middleware.clickjacking.XFrameOptionsMiddleware" in settings.MIDDLEWARE_CLASSES
106+
107+
108+
@register(Tags.security, deploy=True)
109+
def check_security_middleware(app_configs, **kwargs):
110+
passed_check = _security_middleware()
111+
return [] if passed_check else [W001]
112+
113+
114+
@register(Tags.security, deploy=True)
115+
def check_xframe_options_middleware(app_configs, **kwargs):
116+
passed_check = _xframe_middleware()
117+
return [] if passed_check else [W002]
118+
119+
120+
@register(Tags.security, deploy=True)
121+
def check_sts(app_configs, **kwargs):
122+
passed_check = not _security_middleware() or settings.SECURE_HSTS_SECONDS
123+
return [] if passed_check else [W004]
124+
125+
126+
@register(Tags.security, deploy=True)
127+
def check_sts_include_subdomains(app_configs, **kwargs):
128+
passed_check = (
129+
not _security_middleware() or
130+
not settings.SECURE_HSTS_SECONDS or
131+
settings.SECURE_HSTS_INCLUDE_SUBDOMAINS is True
132+
)
133+
return [] if passed_check else [W005]
134+
135+
136+
@register(Tags.security, deploy=True)
137+
def check_content_type_nosniff(app_configs, **kwargs):
138+
passed_check = (
139+
not _security_middleware() or
140+
settings.SECURE_CONTENT_TYPE_NOSNIFF is True
141+
)
142+
return [] if passed_check else [W006]
143+
144+
145+
@register(Tags.security, deploy=True)
146+
def check_xss_filter(app_configs, **kwargs):
147+
passed_check = (
148+
not _security_middleware() or
149+
settings.SECURE_BROWSER_XSS_FILTER is True
150+
)
151+
return [] if passed_check else [W007]
152+
153+
154+
@register(Tags.security, deploy=True)
155+
def check_ssl_redirect(app_configs, **kwargs):
156+
passed_check = (
157+
not _security_middleware() or
158+
settings.SECURE_SSL_REDIRECT is True
159+
)
160+
return [] if passed_check else [W008]
161+
162+
163+
@register(Tags.security, deploy=True)
164+
def check_secret_key(app_configs, **kwargs):
165+
passed_check = (
166+
getattr(settings, 'SECRET_KEY', None) and
167+
len(set(settings.SECRET_KEY)) >= SECRET_KEY_MIN_UNIQUE_CHARACTERS and
168+
len(settings.SECRET_KEY) >= SECRET_KEY_MIN_LENGTH
169+
)
170+
return [] if passed_check else [W009]
171+
172+
173+
@register(Tags.security, deploy=True)
174+
def check_debug(app_configs, **kwargs):
175+
passed_check = not settings.DEBUG
176+
return [] if passed_check else [W018]
177+
178+
179+
@register(Tags.security, deploy=True)
180+
def check_xframe_deny(app_configs, **kwargs):
181+
passed_check = (
182+
not _xframe_middleware() or
183+
settings.X_FRAME_OPTIONS == 'DENY'
184+
)
185+
return [] if passed_check else [W019]

django/core/checks/security/csrf.py

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
from django.conf import settings
2+
3+
from .. import register, Tags, Warning
4+
5+
6+
W003 = Warning(
7+
"You don't appear to be using Django's built-in "
8+
"cross-site request forgery protection via the middleware "
9+
"('django.middleware.csrf.CsrfViewMiddleware' is not in your "
10+
"MIDDLEWARE_CLASSES). Enabling the middleware is the safest approach "
11+
"to ensure you don't leave any holes.",
12+
id='security.W003',
13+
)
14+
15+
W016 = Warning(
16+
"You have 'django.middleware.csrf.CsrfViewMiddleware' in your "
17+
"MIDDLEWARE_CLASSES, but you have not set CSRF_COOKIE_SECURE to True. "
18+
"Using a secure-only CSRF cookie makes it more difficult for network "
19+
"traffic sniffers to steal the CSRF token.",
20+
id='security.W016',
21+
)
22+
23+
W017 = Warning(
24+
"You have 'django.middleware.csrf.CsrfViewMiddleware' in your "
25+
"MIDDLEWARE_CLASSES, but you have not set CSRF_COOKIE_HTTPONLY to True. "
26+
"Using an HttpOnly CSRF cookie makes it more difficult for cross-site "
27+
"scripting attacks to steal the CSRF token.",
28+
id='security.W017',
29+
)
30+
31+
32+
def _csrf_middleware():
33+
return "django.middleware.csrf.CsrfViewMiddleware" in settings.MIDDLEWARE_CLASSES
34+
35+
36+
@register(Tags.security, deploy=True)
37+
def check_csrf_middleware(app_configs, **kwargs):
38+
passed_check = _csrf_middleware()
39+
return [] if passed_check else [W003]
40+
41+
42+
@register(Tags.security, deploy=True)
43+
def check_csrf_cookie_secure(app_configs, **kwargs):
44+
passed_check = (
45+
not _csrf_middleware() or
46+
settings.CSRF_COOKIE_SECURE
47+
)
48+
return [] if passed_check else [W016]
49+
50+
51+
@register(Tags.security, deploy=True)
52+
def check_csrf_cookie_httponly(app_configs, **kwargs):
53+
passed_check = (
54+
not _csrf_middleware() or
55+
settings.CSRF_COOKIE_HTTPONLY
56+
)
57+
return [] if passed_check else [W017]

0 commit comments

Comments
 (0)