1
from django.conf import settings
2
from django.core.exceptions import ImproperlyConfigured
3
from django.core.management import call_command
4
from django.http import HttpResponse
5
from django.test import TestCase, RequestFactory
6
from django.test.utils import override_settings
7
from django.utils.six import StringIO
11
class SecurityMiddlewareTest(TestCase):
14
from djangosecure.middleware import SecurityMiddleware
15
return SecurityMiddleware()
19
def secure_request_kwargs(self):
20
return {"wsgi.url_scheme": "https"}
23
def response(self, *args, **kwargs):
24
headers = kwargs.pop("headers", {})
25
response = HttpResponse(*args, **kwargs)
26
for k, v in headers.items():
31
def process_response(self, *args, **kwargs):
33
if kwargs.pop("secure", False):
34
request_kwargs.update(self.secure_request_kwargs)
35
request = (kwargs.pop("request", None) or
36
self.request.get("/some/url", **request_kwargs))
37
ret = self.middleware.process_request(request)
40
return self.middleware.process_response(
41
request, self.response(*args, **kwargs))
44
request = RequestFactory()
47
def process_request(self, method, *args, **kwargs):
48
if kwargs.pop("secure", False):
49
kwargs.update(self.secure_request_kwargs)
50
req = getattr(self.request, method.lower())(*args, **kwargs)
51
return self.middleware.process_request(req)
54
@override_settings(SECURE_FRAME_DENY=True)
55
def test_frame_deny_on(self):
57
With SECURE_FRAME_DENY True, the middleware adds "x-frame-options:
58
DENY" to the response.
61
self.assertEqual(self.process_response()["x-frame-options"], "DENY")
64
@override_settings(SECURE_FRAME_DENY=True)
65
def test_frame_deny_already_present(self):
67
The middleware will not override an "x-frame-options" header already
68
present in the response.
71
response = self.process_response(
72
headers={"x-frame-options": "SAMEORIGIN"})
73
self.assertEqual(response["x-frame-options"], "SAMEORIGIN")
76
@override_settings(SECURE_FRAME_DENY=True)
77
def test_frame_deny_exempt(self):
79
If the response has the _frame_deny_exempt attribute set to True, the
80
middleware does not add an "x-frame-options" header to the response.
83
response = HttpResponse()
84
response._frame_deny_exempt = True
85
response = self.middleware.process_response("not used", response)
86
self.assertFalse("x-frame-options" in response)
89
@override_settings(SECURE_FRAME_DENY=False)
90
def test_frame_deny_off(self):
92
With SECURE_FRAME_DENY False, the middleware does not add an
93
"x-frame-options" header to the response.
96
self.assertFalse("x-frame-options" in self.process_response())
99
@override_settings(SECURE_HSTS_SECONDS=3600)
100
def test_sts_on(self):
102
With SECURE_HSTS_SECONDS=3600, the middleware adds
103
"strict-transport-security: max-age=3600" to the response.
107
self.process_response(secure=True)["strict-transport-security"],
111
@override_settings(SECURE_HSTS_SECONDS=3600)
112
def test_sts_already_present(self):
114
The middleware will not override a "strict-transport-security" header
115
already present in the response.
118
response = self.process_response(
120
headers={"strict-transport-security": "max-age=7200"})
121
self.assertEqual(response["strict-transport-security"], "max-age=7200")
124
@override_settings(SECURE_HSTS_SECONDS=3600)
125
def test_sts_only_if_secure(self):
127
The "strict-transport-security" header is not added to responses going
128
over an insecure connection.
132
"strict-transport-security" in self.process_response(secure=False))
135
@override_settings(SECURE_HSTS_SECONDS=0)
136
def test_sts_off(self):
138
With SECURE_HSTS_SECONDS of 0, the middleware does not add a
139
"strict-transport-security" header to the response.
143
"strict-transport-security" in self.process_response(secure=True))
147
SECURE_HSTS_SECONDS=600, SECURE_HSTS_INCLUDE_SUBDOMAINS=True)
148
def test_sts_include_subdomains(self):
150
With SECURE_HSTS_SECONDS non-zero and SECURE_HSTS_INCLUDE_SUBDOMAINS
151
True, the middleware adds a "strict-transport-security" header with the
152
"includeSubDomains" tag to the response.
155
response = self.process_response(secure=True)
157
response["strict-transport-security"],
158
"max-age=600; includeSubDomains",
163
SECURE_HSTS_SECONDS=600, SECURE_HSTS_INCLUDE_SUBDOMAINS=False)
164
def test_sts_no_include_subdomains(self):
166
With SECURE_HSTS_SECONDS non-zero and SECURE_HSTS_INCLUDE_SUBDOMAINS
167
False, the middleware adds a "strict-transport-security" header without
168
the "includeSubDomains" tag to the response.
171
response = self.process_response(secure=True)
172
self.assertEqual(response["strict-transport-security"], "max-age=600")
175
@override_settings(SECURE_CONTENT_TYPE_NOSNIFF=True)
176
def test_content_type_on(self):
178
With SECURE_CONTENT_TYPE_NOSNIFF set to True, the middleware adds
179
"x-content-type-options: nosniff" header to the response.
183
self.process_response()["x-content-type-options"],
187
@override_settings(SECURE_CONTENT_TYPE_NO_SNIFF=True)
188
def test_content_type_already_present(self):
190
The middleware will not override an "x-content-type-options" header
191
already present in the response.
194
response = self.process_response(
196
headers={"x-content-type-options": "foo"})
197
self.assertEqual(response["x-content-type-options"], "foo")
200
@override_settings(SECURE_CONTENT_TYPE_NOSNIFF=False)
201
def test_content_type_off(self):
203
With SECURE_CONTENT_TYPE_NOSNIFF False, the middleware does not add an
204
"x-content-type-options" header to the response.
207
self.assertFalse("x-content-type-options" in self.process_response())
210
@override_settings(SECURE_BROWSER_XSS_FILTER=True)
211
def test_xss_filter_on(self):
213
With SECURE_BROWSER_XSS_FILTER set to True, the middleware adds
214
"s-xss-protection: 1; mode=block" header to the response.
218
self.process_response()["x-xss-protection"],
222
@override_settings(SECURE_BROWSER_XSS_FILTER=True)
223
def test_xss_filter_already_present(self):
225
The middleware will not override an "x-xss-protection" header
226
already present in the response.
229
response = self.process_response(
231
headers={"x-xss-protection": "foo"})
232
self.assertEqual(response["x-xss-protection"], "foo")
235
@override_settings(SECURE_BROWSER_XSS_FILTER=False)
236
def test_xss_filter_off(self):
238
With SECURE_BROWSER_XSS_FILTER set to False, the middleware does not add an
239
"x-xss-protection" header to the response.
242
self.assertFalse("x-xss-protection" in self.process_response())
245
@override_settings(SECURE_SSL_REDIRECT=True)
246
def test_ssl_redirect_on(self):
248
With SECURE_SSL_REDIRECT True, the middleware redirects any non-secure
249
requests to the https:// version of the same URL.
252
ret = self.process_request("get", "/some/url?query=string")
253
self.assertEqual(ret.status_code, 301)
255
ret["Location"], "https://testserver/some/url?query=string")
258
@override_settings(SECURE_SSL_REDIRECT=True)
259
def test_no_redirect_ssl(self):
261
The middleware does not redirect secure requests.
264
ret = self.process_request("get", "/some/url", secure=True)
265
self.assertEqual(ret, None)
269
SECURE_SSL_REDIRECT=True, SECURE_REDIRECT_EXEMPT=["^insecure/"])
270
def test_redirect_exempt(self):
272
The middleware does not redirect requests with URL path matching an
276
ret = self.process_request("get", "/insecure/page")
277
self.assertEqual(ret, None)
281
SECURE_SSL_REDIRECT=True, SECURE_SSL_HOST="secure.example.com")
282
def test_redirect_ssl_host(self):
284
The middleware redirects to SECURE_SSL_HOST if given.
287
ret = self.process_request("get", "/some/url")
288
self.assertEqual(ret.status_code, 301)
289
self.assertEqual(ret["Location"], "https://secure.example.com/some/url")
292
@override_settings(SECURE_SSL_REDIRECT=False)
293
def test_ssl_redirect_off(self):
295
With SECURE_SSL_REDIRECT False, the middleware does no redirect.
298
ret = self.process_request("get", "/some/url")
299
self.assertEqual(ret, None)
303
class ProxySecurityMiddlewareTest(SecurityMiddlewareTest):
305
Test that SecurityMiddleware behaves the same even if our "secure request"
306
indicator is a proxy header.
310
self.override = override_settings(
311
SECURE_PROXY_SSL_HEADER=("HTTP_X_FORWARDED_PROTOCOL", "https"))
313
self.override.enable()
317
self.override.disable()
321
def secure_request_kwargs(self):
322
return {"HTTP_X_FORWARDED_PROTOCOL": "https"}
325
def test_is_secure(self):
327
SecurityMiddleware patches request.is_secure() to report ``True`` even
328
with a proxy-header secure request.
331
request = self.request.get("/some/url", **self.secure_request_kwargs)
332
self.middleware.process_request(request)
334
self.assertEqual(request.is_secure(), True)
339
class FrameDenyExemptTest(TestCase):
340
def test_adds_exempt_attr(self):
342
Test that the decorator adds a _frame_deny_exempt attribute to the
343
response. (We test above in the middleware tests that this attribute
344
causes the X-Frame-Options header to not be added.)
347
from djangosecure.decorators import frame_deny_exempt
351
return HttpResponse()
353
self.assertEqual(myview("not used")._frame_deny_exempt, True)
358
return set(["SOME_WARNING"])
360
fake_test.messages = {
361
"SOME_WARNING": "This is the warning message."
365
return set(["OTHER WARNING"])
371
class RunChecksTest(TestCase):
374
from djangosecure.check import run_checks
380
"djangosecure.tests.fake_test",
381
"djangosecure.tests.nomsg_test"])
382
def test_returns_warnings(self):
383
self.assertEqual(self.func(), set(["SOME_WARNING", "OTHER WARNING"]))
387
class CheckSettingsCommandTest(TestCase):
388
def call(self, **options):
389
stdout = options.setdefault("stdout", StringIO())
390
stderr = options.setdefault("stderr", StringIO())
392
call_command("checksecure", **options)
397
return stdout.read(), stderr.read()
400
@override_settings(SECURE_CHECKS=["djangosecure.tests.fake_test"])
401
def test_prints_messages(self):
402
stdout, stderr = self.call()
403
self.assertTrue("This is the warning message." in stderr)
406
@override_settings(SECURE_CHECKS=["djangosecure.tests.nomsg_test"])
407
def test_prints_code_if_no_message(self):
408
stdout, stderr = self.call()
409
self.assertTrue("OTHER WARNING" in stderr)
412
@override_settings(SECURE_CHECKS=["djangosecure.tests.fake_test"])
413
def test_prints_code_if_verbosity_0(self):
414
stdout, stderr = self.call(verbosity=0)
415
self.assertTrue("SOME_WARNING" in stderr)
418
@override_settings(SECURE_CHECKS=["djangosecure.tests.fake_test"])
419
def test_prints_check_names(self):
420
stdout, stderr = self.call()
421
self.assertTrue("djangosecure.tests.fake_test" in stdout)
424
@override_settings(SECURE_CHECKS=["djangosecure.tests.fake_test"])
425
def test_no_verbosity(self):
426
stdout, stderr = self.call(verbosity=0)
427
self.assertEqual(stdout, "")
430
@override_settings(SECURE_CHECKS=["djangosecure.tests.passing_test"])
431
def test_all_clear(self):
432
stdout, stderr = self.call()
433
self.assertTrue("All clear!" in stdout)
437
class CheckSessionCookieSecureTest(TestCase):
440
from djangosecure.check.sessions import check_session_cookie_secure
441
return check_session_cookie_secure
445
SESSION_COOKIE_SECURE=False,
446
INSTALLED_APPS=["django.contrib.sessions"],
447
MIDDLEWARE_CLASSES=[])
448
def test_session_cookie_secure_with_installed_app(self):
450
Warns if SESSION_COOKIE_SECURE is off and "django.contrib.sessions" is
455
self.func(), set(["SESSION_COOKIE_NOT_SECURE_APP_INSTALLED"]))
459
SESSION_COOKIE_SECURE=False,
462
"django.contrib.sessions.middleware.SessionMiddleware"])
463
def test_session_cookie_secure_with_middleware(self):
465
Warns if SESSION_COOKIE_SECURE is off and
466
"django.contrib.sessions.middleware.SessionMiddleware" is in
471
self.func(), set(["SESSION_COOKIE_NOT_SECURE_MIDDLEWARE"]))
475
SESSION_COOKIE_SECURE=False,
476
INSTALLED_APPS=["django.contrib.sessions"],
478
"django.contrib.sessions.middleware.SessionMiddleware"])
479
def test_session_cookie_secure_both(self):
481
If SESSION_COOKIE_SECURE is off and we find both the session app and
482
the middleware, we just provide one common warning.
486
self.func(), set(["SESSION_COOKIE_NOT_SECURE"]))
490
SESSION_COOKIE_SECURE=True,
491
INSTALLED_APPS=["django.contrib.sessions"],
493
"django.contrib.sessions.middleware.SessionMiddleware"])
494
def test_session_cookie_secure_true(self):
496
If SESSION_COOKIE_SECURE is on, there's no warning about it.
499
self.assertEqual(self.func(), set())
503
class CheckSessionCookieHttpOnlyTest(TestCase):
506
from djangosecure.check.sessions import check_session_cookie_httponly
507
return check_session_cookie_httponly
511
SESSION_COOKIE_HTTPONLY=False,
512
INSTALLED_APPS=["django.contrib.sessions"],
513
MIDDLEWARE_CLASSES=[])
514
def test_session_cookie_httponly_with_installed_app(self):
516
Warns if SESSION_COOKIE_HTTPONLY is off and "django.contrib.sessions"
517
is in INSTALLED_APPS.
521
self.func(), set(["SESSION_COOKIE_NOT_HTTPONLY_APP_INSTALLED"]))
525
SESSION_COOKIE_HTTPONLY=False,
528
"django.contrib.sessions.middleware.SessionMiddleware"])
529
def test_session_cookie_httponly_with_middleware(self):
531
Warns if SESSION_COOKIE_HTTPONLY is off and
532
"django.contrib.sessions.middleware.SessionMiddleware" is in
537
self.func(), set(["SESSION_COOKIE_NOT_HTTPONLY_MIDDLEWARE"]))
541
SESSION_COOKIE_HTTPONLY=False,
542
INSTALLED_APPS=["django.contrib.sessions"],
544
"django.contrib.sessions.middleware.SessionMiddleware"])
545
def test_session_cookie_httponly_both(self):
547
If SESSION_COOKIE_HTTPONLY is off and we find both the session app and
548
the middleware, we just provide one common warning.
552
self.func(), set(["SESSION_COOKIE_NOT_HTTPONLY"]))
556
SESSION_COOKIE_HTTPONLY=True,
557
INSTALLED_APPS=["django.contrib.sessions"],
559
"django.contrib.sessions.middleware.SessionMiddleware"])
560
def test_session_cookie_httponly_true(self):
562
If SESSION_COOKIE_HTTPONLY is on, there's no warning about it.
565
self.assertEqual(self.func(), set())
569
class CheckCSRFMiddlewareTest(TestCase):
572
from djangosecure.check.csrf import check_csrf_middleware
573
return check_csrf_middleware
576
@override_settings(MIDDLEWARE_CLASSES=[])
577
def test_no_csrf_middleware(self):
579
self.func(), set(["CSRF_VIEW_MIDDLEWARE_NOT_INSTALLED"]))
583
MIDDLEWARE_CLASSES=["django.middleware.csrf.CsrfViewMiddleware"])
584
def test_with_csrf_middleware(self):
585
self.assertEqual(self.func(), set())
589
class CheckSecurityMiddlewareTest(TestCase):
592
from djangosecure.check.djangosecure import check_security_middleware
593
return check_security_middleware
596
@override_settings(MIDDLEWARE_CLASSES=[])
597
def test_no_security_middleware(self):
599
self.func(), set(["SECURITY_MIDDLEWARE_NOT_INSTALLED"]))
603
MIDDLEWARE_CLASSES=["djangosecure.middleware.SecurityMiddleware"])
604
def test_with_security_middleware(self):
605
self.assertEqual(self.func(), set())
609
class CheckStrictTransportSecurityTest(TestCase):
612
from djangosecure.check.djangosecure import check_sts
616
@override_settings(SECURE_HSTS_SECONDS=0)
617
def test_no_sts(self):
619
self.func(), set(["STRICT_TRANSPORT_SECURITY_NOT_ENABLED"]))
622
@override_settings(SECURE_HSTS_SECONDS=3600)
623
def test_with_sts(self):
624
self.assertEqual(self.func(), set())
628
class CheckStrictTransportSecuritySubdomainsTest(TestCase):
631
from djangosecure.check.djangosecure import check_sts_include_subdomains
632
return check_sts_include_subdomains
635
@override_settings(SECURE_HSTS_INCLUDE_SUBDOMAINS=False)
636
def test_no_sts_subdomains(self):
638
self.func(), set(["STRICT_TRANSPORT_SECURITY_NO_SUBDOMAINS"]))
641
@override_settings(SECURE_HSTS_INCLUDE_SUBDOMAINS=True)
642
def test_with_sts_subdomains(self):
643
self.assertEqual(self.func(), set())
647
class CheckFrameDenyTest(TestCase):
650
from djangosecure.check.djangosecure import check_frame_deny
651
return check_frame_deny
654
@override_settings(SECURE_FRAME_DENY=False)
655
def test_no_frame_deny(self):
657
self.func(), set(["FRAME_DENY_NOT_ENABLED"]))
660
@override_settings(SECURE_FRAME_DENY=True)
661
def test_with_frame_deny(self):
662
self.assertEqual(self.func(), set())
666
class CheckContentTypeNosniffTest(TestCase):
669
from djangosecure.check.djangosecure import check_content_type_nosniff
670
return check_content_type_nosniff
673
@override_settings(SECURE_CONTENT_TYPE_NOSNIFF=False)
674
def test_no_content_type_nosniff(self):
676
self.func(), set(["CONTENT_TYPE_NOSNIFF_NOT_ENABLED"]))
679
@override_settings(SECURE_CONTENT_TYPE_NOSNIFF=True)
680
def test_with_content_type_nosniff(self):
681
self.assertEqual(self.func(), set())
685
class CheckXssFilterTest(TestCase):
688
from djangosecure.check.djangosecure import check_xss_filter
689
return check_xss_filter
692
@override_settings(SECURE_BROWSER_XSS_FILTER=False)
693
def test_no_xss_filter(self):
695
self.func(), set(["BROWSER_XSS_FILTER_NOT_ENABLED"]))
698
@override_settings(SECURE_BROWSER_XSS_FILTER=True)
699
def test_with_xss_filter(self):
700
self.assertEqual(self.func(), set())
704
class CheckSSLRedirectTest(TestCase):
707
from djangosecure.check.djangosecure import check_ssl_redirect
708
return check_ssl_redirect
711
@override_settings(SECURE_SSL_REDIRECT=False)
712
def test_no_sts(self):
714
self.func(), set(["SSL_REDIRECT_NOT_ENABLED"]))
717
@override_settings(SECURE_SSL_REDIRECT=True)
718
def test_with_sts(self):
719
self.assertEqual(self.func(), set())
722
class CheckSecretKeyTest(TestCase):
725
from djangosecure.check.djangosecure import check_secret_key
726
return check_secret_key
729
@override_settings(SECRET_KEY='awcetupav$#!^h9wTUAPCJWE&!T#``Ho;ta9w4tva')
730
def test_okay_secret_key(self):
731
self.assertEqual(self.func(), set())
734
@override_settings(SECRET_KEY='')
735
def test_empty_secret_key(self):
736
self.assertEqual(self.func(), set(['BAD_SECRET_KEY']))
739
@override_settings(SECRET_KEY=None)
740
def test_missing_secret_key(self):
741
del settings.SECRET_KEY
742
self.assertEqual(self.func(), set(['BAD_SECRET_KEY']))
745
@override_settings(SECRET_KEY=None)
746
def test_none_secret_key(self):
747
self.assertEqual(self.func(), set(['BAD_SECRET_KEY']))
750
@override_settings(SECRET_KEY='bla bla')
751
def test_low_entropy_secret_key(self):
752
self.assertEqual(self.func(), set(['BAD_SECRET_KEY']))
756
class ConfTest(TestCase):
757
def test_no_fallback(self):
759
Accessing a setting without a default value raises in
760
ImproperlyConfigured.
763
from djangosecure.conf import conf
765
self.assertRaises(ImproperlyConfigured, getattr, conf, "HAS_NO_DEFAULT")
768
def test_defaults(self):
769
from djangosecure.conf import conf
775
"djangosecure.check.csrf.check_csrf_middleware",
776
"djangosecure.check.sessions.check_session_cookie_secure",
777
"djangosecure.check.sessions.check_session_cookie_httponly",
778
"djangosecure.check.djangosecure.check_security_middleware",
779
"djangosecure.check.djangosecure.check_sts",
780
"djangosecure.check.djangosecure.check_sts_include_subdomains",
781
"djangosecure.check.djangosecure.check_frame_deny",
782
"djangosecure.check.djangosecure.check_content_type_nosniff",
783
"djangosecure.check.djangosecure.check_xss_filter",
784
"djangosecure.check.djangosecure.check_ssl_redirect",
785
"djangosecure.check.djangosecure.check_secret_key",
787
"SECURE_HSTS_SECONDS": 0,
788
"SECURE_HSTS_INCLUDE_SUBDOMAINS": False,
789
"SECURE_FRAME_DENY": False,
790
"SECURE_CONTENT_TYPE_NOSNIFF": False,
791
"SECURE_BROWSER_XSS_FILTER": False,
792
"SECURE_SSL_REDIRECT": False,
793
"SECURE_SSL_HOST": None,
794
"SECURE_REDIRECT_EXEMPT": [],
795
"SECURE_PROXY_SSL_HEADER": None,