2017-07-14 21:45:21 -04:00
|
|
|
from collections import OrderedDict
|
|
|
|
|
2017-07-15 01:23:19 -04:00
|
|
|
from django.core.exceptions import MiddlewareNotUsed
|
|
|
|
from django.http import HttpResponse
|
|
|
|
from django.test import SimpleTestCase, RequestFactory, override_settings
|
|
|
|
from django.utils.decorators import decorator_from_middleware_with_args
|
2017-07-14 21:45:21 -04:00
|
|
|
|
2017-07-15 02:45:23 -04:00
|
|
|
from csp_advanced.csp import CSPCompiler, InvalidCSPError
|
2017-07-15 01:23:19 -04:00
|
|
|
from csp_advanced.middleware import AdvancedCSPMiddleware
|
2017-07-15 02:45:23 -04:00
|
|
|
from csp_advanced.utils import call_csp_dict, merge_csp_dict, is_callable_csp_dict
|
2017-07-14 21:45:21 -04:00
|
|
|
|
|
|
|
|
|
|
|
class CSPCompileTest(SimpleTestCase):
|
|
|
|
def test_fetch(self):
|
|
|
|
self.assertEqual(CSPCompiler({
|
|
|
|
'script-src': ['self', 'https://dmoj.ca', 'nonce-123'],
|
|
|
|
}).compile(), "script-src 'self' https://dmoj.ca 'nonce-123'")
|
|
|
|
|
|
|
|
with self.assertRaises(InvalidCSPError):
|
|
|
|
CSPCompiler({
|
|
|
|
'script-src': 'https://dmoj.ca',
|
|
|
|
}).compile()
|
|
|
|
|
|
|
|
def test_sandbox(self):
|
|
|
|
self.assertEqual(CSPCompiler({
|
|
|
|
'sandbox': ['allow-same-origin', 'allow-scripts'],
|
|
|
|
}).compile(), "sandbox allow-same-origin allow-scripts")
|
|
|
|
|
|
|
|
with self.assertRaises(InvalidCSPError):
|
|
|
|
CSPCompiler({
|
|
|
|
'sandbox': ['allow-invalid', 'allow-scripts'],
|
|
|
|
}).compile()
|
|
|
|
|
|
|
|
with self.assertRaises(InvalidCSPError):
|
|
|
|
CSPCompiler({
|
|
|
|
'sandbox': 'allow-scripts',
|
|
|
|
}).compile()
|
|
|
|
|
|
|
|
def test_report_uri(self):
|
|
|
|
self.assertEqual(CSPCompiler({
|
|
|
|
'report-uri': '/dev/null',
|
|
|
|
}).compile(), "report-uri /dev/null")
|
|
|
|
|
|
|
|
with self.assertRaises(InvalidCSPError):
|
|
|
|
CSPCompiler({'report-uri': []}).compile()
|
|
|
|
|
|
|
|
def test_require_sri_for(self):
|
|
|
|
self.assertEqual(CSPCompiler({
|
|
|
|
'require-sri-for': 'script style',
|
|
|
|
}).compile(), "require-sri-for script style")
|
|
|
|
|
|
|
|
with self.assertRaises(InvalidCSPError):
|
|
|
|
CSPCompiler({'require-sri-for': []}).compile()
|
|
|
|
|
|
|
|
with self.assertRaises(InvalidCSPError):
|
|
|
|
CSPCompiler({'require-sri-for': 'bad'}).compile()
|
|
|
|
|
|
|
|
def test_upgrade_insecure_requests(self):
|
|
|
|
self.assertEqual(CSPCompiler({
|
|
|
|
'upgrade-insecure-requests': True,
|
|
|
|
}).compile(), "upgrade-insecure-requests")
|
|
|
|
|
|
|
|
self.assertEqual(CSPCompiler({
|
|
|
|
'upgrade-insecure-requests': False,
|
|
|
|
}).compile(), '')
|
|
|
|
|
|
|
|
def test_integration(self):
|
|
|
|
self.assertEqual(CSPCompiler(OrderedDict([
|
|
|
|
('style-src', ['self']),
|
|
|
|
('script-src', ['self', 'https://dmoj.ca']),
|
|
|
|
('frame-src', ['none']),
|
|
|
|
('plugin-types', ['application/pdf']),
|
|
|
|
('block-all-mixed-content', True),
|
|
|
|
('upgrade-insecure-requests', False),
|
|
|
|
('sandbox', ['allow-scripts']),
|
|
|
|
('report-uri', '/dev/null')
|
|
|
|
])).compile(),
|
|
|
|
"style-src 'self'; script-src 'self' https://dmoj.ca; frame-src 'none'; "
|
|
|
|
"plugin-types application/pdf; block-all-mixed-content; sandbox allow-scripts; "
|
|
|
|
"report-uri /dev/null")
|
2017-07-14 22:55:56 -04:00
|
|
|
|
|
|
|
|
|
|
|
class CallableCSPDictTest(SimpleTestCase):
|
2017-07-15 00:32:06 -04:00
|
|
|
request = object()
|
|
|
|
response = object()
|
|
|
|
|
|
|
|
def make_request_taker(self, output):
|
|
|
|
def func(request, response):
|
|
|
|
self.assertEqual(request, self.request)
|
|
|
|
self.assertEqual(response, self.response)
|
|
|
|
return output
|
|
|
|
return func
|
|
|
|
|
2017-07-14 22:55:56 -04:00
|
|
|
def test_callable(self):
|
2017-07-15 01:23:19 -04:00
|
|
|
self.assertEqual(call_csp_dict(
|
2017-07-15 00:32:06 -04:00
|
|
|
self.make_request_taker({'key': 'value'}), self.request, self.response
|
|
|
|
), {'key': 'value'})
|
2017-07-14 22:55:56 -04:00
|
|
|
|
|
|
|
def test_normal_dict(self):
|
2017-07-15 01:23:19 -04:00
|
|
|
self.assertEqual(call_csp_dict({'key': 'value'}, None, None), {'key': 'value'})
|
2017-07-14 22:55:56 -04:00
|
|
|
|
|
|
|
def test_callable_entry(self):
|
2017-07-15 01:23:19 -04:00
|
|
|
self.assertEqual(call_csp_dict(
|
2017-07-15 00:32:06 -04:00
|
|
|
{'key': self.make_request_taker('value')}, self.request, self.response
|
|
|
|
), {'key': 'value'})
|
2017-07-14 22:55:56 -04:00
|
|
|
|
|
|
|
def test_mixed_entry(self):
|
2017-07-15 01:23:19 -04:00
|
|
|
self.assertEqual(call_csp_dict({
|
2017-07-15 00:32:06 -04:00
|
|
|
'key': self.make_request_taker('value'),
|
2017-07-14 22:55:56 -04:00
|
|
|
'name': 'mixed',
|
2017-07-15 00:32:06 -04:00
|
|
|
}, self.request, self.response), {
|
2017-07-14 22:55:56 -04:00
|
|
|
'key': 'value',
|
|
|
|
'name': 'mixed'
|
|
|
|
})
|
|
|
|
|
2017-07-15 01:23:19 -04:00
|
|
|
def test_is_callable(self):
|
|
|
|
self.assertTrue(is_callable_csp_dict(self.make_request_taker({})))
|
|
|
|
self.assertTrue(is_callable_csp_dict({'key': self.make_request_taker('value')}))
|
|
|
|
self.assertFalse(is_callable_csp_dict({}))
|
|
|
|
self.assertFalse(is_callable_csp_dict({'key': 'value'}))
|
|
|
|
self.assertFalse(is_callable_csp_dict(None))
|
|
|
|
|
2017-07-14 22:55:56 -04:00
|
|
|
|
|
|
|
class MergeCSPDictTest(SimpleTestCase):
|
|
|
|
def test_null(self):
|
|
|
|
test = {'key': 'value'}
|
|
|
|
self.assertEqual(merge_csp_dict(test, {}), test)
|
|
|
|
|
|
|
|
def test_distinct_key(self):
|
|
|
|
self.assertEqual(merge_csp_dict({'spam': 1}, {'ham': 2}), {'spam': 1, 'ham': 2})
|
|
|
|
|
|
|
|
def test_scalar_override(self):
|
|
|
|
self.assertEqual(merge_csp_dict({'spam': 1}, {'spam': 2}), {'spam': 2})
|
|
|
|
|
|
|
|
def test_list_override(self):
|
|
|
|
orig = [1]
|
|
|
|
self.assertEqual(merge_csp_dict({'spam': orig}, {'spam': [2]}), {'spam': [1, 2]})
|
|
|
|
self.assertEqual(orig, [1])
|
|
|
|
|
|
|
|
def test_set_override(self):
|
|
|
|
orig = {1}
|
|
|
|
self.assertEqual(merge_csp_dict({'spam': orig}, {'spam': [2]}), {'spam': {1, 2}})
|
|
|
|
self.assertEqual(orig, {1})
|
|
|
|
|
|
|
|
def test_tuple_override(self):
|
|
|
|
self.assertEqual(merge_csp_dict({'spam': (1,)}, {'spam': (2,)}), {'spam': (1, 2)})
|
2017-07-15 01:23:19 -04:00
|
|
|
|
|
|
|
|
|
|
|
class TestMiddleware(SimpleTestCase):
|
|
|
|
decorator_factory = decorator_from_middleware_with_args(AdvancedCSPMiddleware)
|
|
|
|
|
|
|
|
def setUp(self):
|
|
|
|
self.factory = RequestFactory()
|
|
|
|
|
|
|
|
def make_ok_view(self):
|
|
|
|
@self.decorator_factory()
|
|
|
|
def view(request):
|
|
|
|
return HttpResponse('ok')
|
|
|
|
return view
|
|
|
|
|
|
|
|
def get_request(self):
|
|
|
|
return self.factory.get('/')
|
|
|
|
|
|
|
|
def test_no_csp(self):
|
|
|
|
self.assertRaises(MiddlewareNotUsed, self.decorator_factory)
|
|
|
|
|
|
|
|
@override_settings(ADVANCED_CSP={'script-src': ['self']})
|
|
|
|
def test_setting_csp(self):
|
|
|
|
self.assertEqual(self.make_ok_view()(self.get_request())['Content-Security-Policy'], "script-src 'self'")
|
|
|
|
|
2017-07-15 02:36:31 -04:00
|
|
|
@override_settings(ADVANCED_CSP='verbatim bad csp')
|
|
|
|
def test_setting_str(self):
|
|
|
|
self.assertEqual(self.make_ok_view()(self.get_request())['Content-Security-Policy'], 'verbatim bad csp')
|
|
|
|
|
|
|
|
@override_settings(ADVANCED_CSP={'script-src': ['self']})
|
|
|
|
def test_csp_exists(self):
|
|
|
|
@self.decorator_factory()
|
|
|
|
def view(request):
|
|
|
|
response = HttpResponse()
|
|
|
|
response['Content-Security-Policy'] = 'verbatim bad csp'
|
|
|
|
return response
|
|
|
|
self.assertEqual(view(self.get_request())['Content-Security-Policy'], 'verbatim bad csp')
|
|
|
|
|
2017-07-15 01:44:03 -04:00
|
|
|
@override_settings(ADVANCED_CSP={'bad': ['self']})
|
|
|
|
def test_invalid_csp(self):
|
|
|
|
self.assertFalse('Content-Security-Policy' in self.make_ok_view()(self.get_request()))
|
|
|
|
|
2017-07-15 01:23:19 -04:00
|
|
|
@override_settings(ADVANCED_CSP_REPORT_ONLY={'default-src': ['http://dmoj.ca']})
|
|
|
|
def test_setting_csp_report(self):
|
|
|
|
self.assertEqual(self.make_ok_view()(self.get_request())['Content-Security-Policy-Report-Only'],
|
|
|
|
"default-src http://dmoj.ca")
|
|
|
|
|
|
|
|
@override_settings(ADVANCED_CSP={'script-src': ['self']},
|
|
|
|
ADVANCED_CSP_REPORT_ONLY={'default-src': ['http://dmoj.ca']})
|
|
|
|
def test_setting_both(self):
|
|
|
|
response = self.make_ok_view()(self.get_request())
|
|
|
|
self.assertEqual(response['Content-Security-Policy'], "script-src 'self'")
|
|
|
|
self.assertEqual(response['Content-Security-Policy-Report-Only'], 'default-src http://dmoj.ca')
|
|
|
|
|
|
|
|
@override_settings(ADVANCED_CSP={'script-src': ['self']})
|
|
|
|
def test_merge_csp_same(self):
|
|
|
|
@self.decorator_factory()
|
|
|
|
def view(request):
|
|
|
|
response = HttpResponse()
|
|
|
|
response.csp = {'script-src': ['https://dmoj.ca']}
|
|
|
|
return response
|
|
|
|
self.assertEqual(view(self.get_request())['Content-Security-Policy'], "script-src 'self' https://dmoj.ca")
|
|
|
|
|
|
|
|
@override_settings(ADVANCED_CSP={'script-src': ['self']})
|
|
|
|
def test_merge_csp_different(self):
|
|
|
|
@self.decorator_factory()
|
|
|
|
def view(request):
|
|
|
|
response = HttpResponse()
|
|
|
|
response.csp = {'style-src': ['https://dmoj.ca']}
|
|
|
|
return response
|
|
|
|
self.assertEqual(view(self.get_request())['Content-Security-Policy'],
|
|
|
|
"script-src 'self'; style-src https://dmoj.ca")
|
|
|
|
|
|
|
|
@override_settings(ADVANCED_CSP={'script-src': ['self']})
|
|
|
|
def test_override_csp_explicit(self):
|
|
|
|
@self.decorator_factory()
|
|
|
|
def view(request):
|
|
|
|
response = HttpResponse()
|
|
|
|
response.csp = {'style-src': ['none'], 'override': True}
|
|
|
|
return response
|
|
|
|
self.assertEqual(view(self.get_request())['Content-Security-Policy'], "style-src 'none'")
|
|
|
|
|
2017-07-15 02:36:31 -04:00
|
|
|
@override_settings(ADVANCED_CSP={'script-src': ['self']})
|
|
|
|
def test_remove_csp(self):
|
|
|
|
@self.decorator_factory()
|
|
|
|
def view(request):
|
|
|
|
response = HttpResponse()
|
|
|
|
response.csp = {'override': True}
|
|
|
|
return response
|
|
|
|
self.assertFalse('Content-Security-Policy' in view(self.get_request()))
|
|
|
|
|
2017-07-15 01:23:19 -04:00
|
|
|
@override_settings(ADVANCED_CSP_REPORT_ONLY={'script-src': ['self']})
|
|
|
|
def test_override_csp_to_report_explicit(self):
|
|
|
|
@self.decorator_factory()
|
|
|
|
def view(request):
|
|
|
|
response = HttpResponse()
|
|
|
|
response.csp = {'style-src': ['none'], 'override': True}
|
|
|
|
return response
|
|
|
|
self.assertEqual(view(self.get_request())['Content-Security-Policy-Report-Only'], "style-src 'none'")
|
|
|
|
|
|
|
|
@override_settings(ADVANCED_CSP_REPORT_ONLY={'script-src': ['self']})
|
|
|
|
def test_override_csp_report_both_explicit(self):
|
|
|
|
@self.decorator_factory()
|
|
|
|
def view(request):
|
|
|
|
response = HttpResponse()
|
|
|
|
response.csp = {'style-src': ['none'], 'override': True}
|
|
|
|
response.csp_report = {'script-src': ['none'], 'override': True}
|
|
|
|
return response
|
|
|
|
|
|
|
|
response = view(self.get_request())
|
|
|
|
self.assertEqual(response['Content-Security-Policy-Report-Only'], "script-src 'none'")
|
2017-07-15 02:36:31 -04:00
|
|
|
self.assertFalse('Content-Security-Policy' in response)
|
2017-07-15 01:23:19 -04:00
|
|
|
|
|
|
|
@override_settings(ADVANCED_CSP_REPORT_ONLY={'script-src': ['self']})
|
|
|
|
def test_override_csp_report_only_explicit(self):
|
|
|
|
@self.decorator_factory()
|
|
|
|
def view(request):
|
|
|
|
response = HttpResponse()
|
|
|
|
response.csp_report = {'script-src': ['none'], 'override': True}
|
|
|
|
return response
|
|
|
|
|
|
|
|
response = view(self.get_request())
|
|
|
|
self.assertEqual(response['Content-Security-Policy-Report-Only'], "script-src 'none'")
|
2017-07-15 02:36:31 -04:00
|
|
|
self.assertFalse('Content-Security-Policy' in response)
|