mirror of
https://github.com/quantum5/django-csp-advanced.git
synced 2025-04-24 11:22:00 -04:00
Initial version with CSP compiling and testing.
This commit is contained in:
parent
711f9a34d1
commit
341ff8a33d
0
csp_advanced/__init__.py
Normal file
0
csp_advanced/__init__.py
Normal file
3
csp_advanced/admin.py
Normal file
3
csp_advanced/admin.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
7
csp_advanced/apps.py
Normal file
7
csp_advanced/apps.py
Normal 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
130
csp_advanced/csp.py
Normal 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)))
|
0
csp_advanced/middleware.py
Normal file
0
csp_advanced/middleware.py
Normal file
0
csp_advanced/migrations/__init__.py
Normal file
0
csp_advanced/migrations/__init__.py
Normal file
5
csp_advanced/models.py
Normal file
5
csp_advanced/models.py
Normal 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
75
csp_advanced/tests.py
Normal 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
3
csp_advanced/views.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
Loading…
Reference in a new issue