Initial version with CSP compiling and testing.

This commit is contained in:
Quantum 2017-07-14 21:45:21 -04:00
parent 711f9a34d1
commit 341ff8a33d
9 changed files with 223 additions and 0 deletions

0
csp_advanced/__init__.py Normal file
View file

3
csp_advanced/admin.py Normal file
View file

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

7
csp_advanced/apps.py Normal file
View file

@ -0,0 +1,7 @@
from __future__ import unicode_literals
from django.apps import AppConfig
class CspAdvancedConfig(AppConfig):
name = 'csp_advanced'

130
csp_advanced/csp.py Normal file
View file

@ -0,0 +1,130 @@
from itertools import chain
class InvalidCSPError(ValueError):
pass
class CSPCompiler(object):
CSP_LISTS = {
# Fetch directives:
'connect-src',
'child-src',
'default-src',
'font-src',
'frame-src',
'img-src',
'manifest-src',
'media-src',
'object-src',
'script-src',
'style-src',
'worker-src',
# Navigation directives:
'form-action',
'frame-ancestors',
# Document directives:
'base-uri',
'plugin-types',
}
CSP_BOOLEAN = {
'upgrade-insecure-requests',
'block-all-mixed-content',
}
CSP_FETCH_SPECIAL = {
'self',
'none',
'unsafe-inline',
'unsafe-eval',
'strict-dynamic',
}
CSP_PREFIX_SPECIAL = (
'nonce-',
'sha256-',
'sha384-',
'sha512-'
)
CSP_SANDBOX_VALID = {
'allow-forms',
'allow-modals',
'allow-orientation-lock',
'allow-pointer-lock',
'allow-popups',
'allow-popups-to-escape-sandbox',
'allow-presentation',
'allow-same-origin',
'allow-scripts',
'allow-top-navigation',
}
CSP_REQUIRE_SRI_VALID = {
'script',
'style',
'script style',
}
def __init__(self, csp_dict):
self.csp = csp_dict
def compile(self):
pieces = []
for name, value in self.csp.iteritems():
if name in self.CSP_LISTS:
if value:
pieces.append(self.compile_list(name, value))
elif name in self.CSP_BOOLEAN:
if value:
pieces.append(name)
elif name == 'sandbox':
if value:
pieces.append(self.compile_sandbox(value))
elif name == 'report-uri':
pieces.append(self.compile_report_uri(value))
elif name == 'require-sri-for':
pieces.append(self.compile_require_sri_for(value))
else:
raise InvalidCSPError('Unknown directive: %s' % (name,))
return '; '.join(pieces)
def compile_list(self, name, value_list):
self.ensure_list(name, value_list)
values = [name]
for value in value_list:
if value in self.CSP_FETCH_SPECIAL or value.startswith(self.CSP_PREFIX_SPECIAL):
values.append("'%s'" % value)
else:
values.append(value)
return ' '.join(values)
def compile_sandbox(self, values):
self.ensure_list('sandbox', values)
for value in values:
if value not in self.CSP_SANDBOX_VALID:
raise InvalidCSPError('Unknown sandbox value: %s' % (value,))
return ' '.join(chain(['sandbox'], values))
def compile_report_uri(self, value):
self.ensure_str('report-uri', value)
return 'report-uri %s' % value
def compile_require_sri_for(self, value):
self.ensure_str('require-sri-for', value)
if value not in self.CSP_REQUIRE_SRI_VALID:
raise InvalidCSPError('Unknown require-sri-for value: %s' % (value,))
return 'require-sri-for %s' % value
@staticmethod
def ensure_list(name, value):
if not isinstance(value, (list, tuple, set)):
raise InvalidCSPError('Values for %s must be list-like type, not %s', (name, type(value)))
@staticmethod
def ensure_str(name, value):
if not isinstance(value, basestring):
raise InvalidCSPError('Values for %s must be a string type, not %s', (name, type(value)))

View file

View file

5
csp_advanced/models.py Normal file
View file

@ -0,0 +1,5 @@
from __future__ import unicode_literals
from django.db import models
# Create your models here.

75
csp_advanced/tests.py Normal file
View file

@ -0,0 +1,75 @@
from collections import OrderedDict
from django.test import SimpleTestCase
from csp import CSPCompiler, InvalidCSPError
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")

3
csp_advanced/views.py Normal file
View file

@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.