Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GH 36 #179

Closed
wants to merge 35 commits into from
Closed

GH 36 #179

Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
3227426
GH-36 Update settings for multi-policy support
DylanYoung Mar 6, 2020
2d7aa80
GH-36 Update utils to handle multiple policies
DylanYoung Mar 6, 2020
290a57d
GH-36 Update csp decorators for multi-policy support
DylanYoung Mar 6, 2020
91402da
GH-36 Update middleware for multi-policy support
DylanYoung Mar 7, 2020
83e2f61
GH-36 Update tests to use new multi-policy format
DylanYoung Mar 10, 2020
ad9cbae
GH-36 Add csp_select and csp_append decorators
DylanYoung May 24, 2022
c6cfa87
GH-36 Add tests for multi-policy support and csp_append/csp_select de…
DylanYoung May 24, 2022
6c25fc9
GH-36 Update docs for multi-policy support
DylanYoung Mar 10, 2020
09a0ce6
GH-36 Update CHANGES for multi-policy support
DylanYoung May 24, 2022
e2e66a9
TEMP: Add utils tests
DylanYoung May 24, 2022
90b72fe
Remove legacy django and python support (GH-36)
DylanYoung May 24, 2022
1a7f979
Exclude docs/conf.py from tests (GH-36)
DylanYoung Mar 9, 2020
29efdf7
TODO: upgrade test deps
DylanYoung May 23, 2022
858ec86
Fix docs build warnings (GH-36)
DylanYoung May 24, 2022
6c0de34
TEMP Update docs
DylanYoung May 24, 2022
02d06bc
Add docs to tox.ini (GH-36)
DylanYoung May 24, 2022
c7bde5c
GH-36 Update RateLimitedCSPMiddleware to support csp_select decorator
DylanYoung May 24, 2022
50de343
fixup! GH-36 Update CHANGES for multi-policy support
DylanYoung May 25, 2022
9fff323
Deprecate block-all-mixed-content (GH-36)
DylanYoung May 25, 2022
2628ac4
fixup! GH-36 Update middleware for multi-policy support
DylanYoung May 25, 2022
16b0e49
fixup! GH-36 Update tests to use new multi-policy format
DylanYoung May 25, 2022
fcc2d6f
fixup! GH-36 Add tests for multi-policy support and csp_append/csp_se…
DylanYoung May 25, 2022
76e1894
GH-36 Disallow mixing deprecated settings with CSP_POLICY_DEFINITIONS
DylanYoung May 25, 2022
7da7eea
fixup! GH-36 Update tests to use new multi-policy format
DylanYoung May 25, 2022
cdd9bb7
fixup! TODO: upgrade test deps
DylanYoung May 25, 2022
fb8be39
fixup! GH-36 Update settings for multi-policy support
DylanYoung May 25, 2022
a6685c3
fixup! GH-36 Add tests for multi-policy support and csp_append/csp_se…
DylanYoung May 25, 2022
a218956
Refactor CSPMiddleware for extensibility (GH-36)
DylanYoung May 26, 2022
22f7301
fixup! GH-36 Update settings for multi-policy support
DylanYoung May 26, 2022
eb66ddc
fixup! GH-36 Add tests for multi-policy support and csp_append/csp_se…
DylanYoung May 26, 2022
3d157cc
fixup! TEMP: Add utils tests
DylanYoung May 26, 2022
cf88db5
fixup! GH-36 Update csp decorators for multi-policy support
DylanYoung May 26, 2022
0a947e2
fixup! GH-36 Update utils to handle multiple policies
DylanYoung May 26, 2022
0b1b649
fixup! GH-36 Disallow mixing deprecated settings with CSP_POLICY_DEFI…
DylanYoung May 26, 2022
1cc55a3
fixup! GH-36 Update docs for multi-policy support
DylanYoung May 26, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ Next
- Drop support for EOL Python <3.6 and Django <2.2 versions
- Rename default branch to main
- Fix capturing brackets in script template tags
- Add support for multiple content security policies
- Deprecate single policy settings. Policies are now
configured through two settings: CSP_POLICIES and CSP_POLICY_DEFINITIONS.

3.7
===
Expand Down
26 changes: 26 additions & 0 deletions csp/conf/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from . import defaults


DIRECTIVES = set(defaults.POLICY)
PSEUDO_DIRECTIVES = {d for d in DIRECTIVES if '_' in d}


def setting_to_directive(setting, value, prefix='CSP_'):
setting = setting[len(prefix):].lower()
if setting not in PSEUDO_DIRECTIVES:
setting = setting.replace('_', '-')
assert setting in DIRECTIVES
if isinstance(value, str):
value = [value]
return setting, value


def directive_to_setting(directive, prefix='CSP_'):
setting = '{}{}'.format(
prefix,
directive.replace('-', '_').upper()
)
return setting


LEGACY_KWARGS = {directive_to_setting(d, prefix='') for d in DIRECTIVES}
44 changes: 44 additions & 0 deletions csp/conf/defaults.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
POLICIES = ('default',)

POLICY = {
# Fetch Directives
'child-src': None,
'connect-src': None,
'default-src': ("'self'",),
'script-src': None,
'script-src-attr': None,
'script-src-elem': None,
'object-src': None,
'style-src': None,
'style-src-attr': None,
'style-src-elem': None,
'font-src': None,
'frame-src': None,
'img-src': None,
'manifest-src': None,
'media-src': None,
'prefetch-src': None,
'worker-src': None,
# Document Directives
'base-uri': None,
'plugin-types': None,
'sandbox': None,
# Navigation Directives
'form-action': None,
'frame-ancestors': None,
'navigate-to': None,
# Reporting Directives
'report-uri': None,
'report-to': None,
'require-sri-for': None,
# Trusted Types Directives
'require-trusted-types-for': None,
'trusted-types': None,
# Other Directives
'upgrade-insecure-requests': False,
'block-all-mixed-content': False,
# Pseudo Directives
'report_only': False,
'include_nonce_in': ('default-src',),
'exclude_url_prefixes': (),
}
51 changes: 51 additions & 0 deletions csp/conf/deprecation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import warnings

from django.conf import settings
from django.core.exceptions import ImproperlyConfigured

from . import (
setting_to_directive,
directive_to_setting,
DIRECTIVES,
)


LEGACY_SETTINGS_NAMES_DEPRECATION_WARNING = (
'The following settings are deprecated: %s. '
'Use CSP_POLICY_DEFINITIONS and CSP_POLICIES instead.'
)


_LEGACY_SETTINGS = {
directive_to_setting(directive) for directive in DIRECTIVES
}


def _handle_legacy_settings(csp, allow_legacy):
"""
Custom defaults allow you to set values for csp directives
that will apply to all CSPs defined in CSP_DEFINITIONS, avoiding
repetition and allowing custom default values.
"""
legacy_names = (
_LEGACY_SETTINGS
& set(s for s in dir(settings) if s.startswith('CSP_'))
)
if not legacy_names:
return

if not allow_legacy:
raise ImproperlyConfigured(
"Settings CSP_POLICY_DEFINITIONS is not allowed with following "
"deprecated settings: %s" % ", ".join(legacy_names)
)

warnings.warn(
LEGACY_SETTINGS_NAMES_DEPRECATION_WARNING % ', '.join(legacy_names),
DeprecationWarning,
)
legacy_csp = (
setting_to_directive(name, value=getattr(settings, name))
for name in legacy_names if name not in csp
)
csp.update(legacy_csp)
78 changes: 68 additions & 10 deletions csp/decorators.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
from functools import wraps
from itertools import chain

from .utils import (
get_declared_policies,
_policies_from_args_and_kwargs,
_policies_from_names_and_kwargs,
)


def csp_exempt(f):
Expand All @@ -10,8 +17,25 @@ def _wrapped(*a, **kw):
return _wrapped


def csp_update(**kwargs):
update = dict((k.lower().replace('_', '-'), v) for k, v in kwargs.items())
def csp_select(*names):
"""
Trim or add additional named policies.
"""
def decorator(f):
@wraps(f)
def _wrapped(*a, **kw):
r = f(*a, **kw)
r._csp_select = names
return r
return _wrapped
return decorator


def csp_update(csp_names=('default',), **kwargs):
update = _policies_from_names_and_kwargs(
csp_names,
kwargs,
)

def decorator(f):
@wraps(f)
Expand All @@ -23,8 +47,11 @@ def _wrapped(*a, **kw):
return decorator


def csp_replace(**kwargs):
replace = dict((k.lower().replace('_', '-'), v) for k, v in kwargs.items())
def csp_replace(csp_names=('default',), **kwargs):
replace = _policies_from_names_and_kwargs(
csp_names,
kwargs,
)

def decorator(f):
@wraps(f)
Expand All @@ -36,12 +63,43 @@ def _wrapped(*a, **kw):
return decorator


def csp(**kwargs):
config = dict(
(k.lower().replace('_', '-'), [v] if isinstance(v, str) else v)
for k, v
in kwargs.items()
)
def csp_append(*args, **kwargs):
append = _policies_from_args_and_kwargs(args, kwargs)

def decorator(f):
@wraps(f)
def _wrapped(*a, **kw):
r = f(*a, **kw)
# TODO: these decorators would interact more smoothly and
# be more performant if we recorded the result on the function.
if hasattr(r, "_csp_config"):
r._csp_config.update({
name: policy for name, policy in append.items()
if name not in r._csp_config
})
select = getattr(r, "_csp_select", None)
if select:
select = list(select)
r._csp_select = tuple(chain(
select,
(name for name in append if name not in select),
))
else:
r._csp_config = append
select = getattr(r, "_csp_select", None)
if not select:
select = get_declared_policies()
r._csp_select = tuple(chain(
select,
(name for name in append if name not in select),
))
return r
return _wrapped
return decorator


def csp(*args, **kwargs):
config = _policies_from_args_and_kwargs(args, kwargs)

def decorator(f):
@wraps(f)
Expand Down
77 changes: 36 additions & 41 deletions csp/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,16 @@

import os
import base64
from collections import defaultdict
from functools import partial

from django.conf import settings
from django.utils.deprecation import MiddlewareMixin
from django.utils.functional import SimpleLazyObject

try:
from django.utils.six.moves import http_client
except ImportError:
# django 3.x removed six
import http.client as http_client

try:
from django.utils.deprecation import MiddlewareMixin
except ImportError:
class MiddlewareMixin(object):
"""
If this middleware doesn't exist, this is an older version of django
and we don't need it.
"""
pass

from csp.utils import build_policy
from .utils import (
build_policy, EXEMPTED_DEBUG_CODES, HTTP_HEADERS,
)


class CSPMiddleware(MiddlewareMixin):
Expand Down Expand Up @@ -54,36 +42,43 @@ def process_response(self, request, response):
if getattr(response, '_csp_exempt', False):
return response

# Check for ignored path prefix.
prefixes = getattr(settings, 'CSP_EXCLUDE_URL_PREFIXES', ())
if request.path_info.startswith(prefixes):
return response

# Check for debug view
status_code = response.status_code
exempted_debug_codes = (
http_client.INTERNAL_SERVER_ERROR,
http_client.NOT_FOUND,
)
if status_code in exempted_debug_codes and settings.DEBUG:
if response.status_code in EXEMPTED_DEBUG_CODES and settings.DEBUG:
return response

header = 'Content-Security-Policy'
if getattr(settings, 'CSP_REPORT_ONLY', False):
header += '-Report-Only'

if header in response:
existing_headers = {
header for header in HTTP_HEADERS if header in response
}
if len(existing_headers) == len(HTTP_HEADERS):
# Don't overwrite existing headers.
return response

response[header] = self.build_policy(request, response)

headers = defaultdict(list)
path_info = request.path_info

for csp, report_only, exclude_prefixes in self.build_policy(
request, response,
):
# Check for ignored path prefix.
for prefix in exclude_prefixes:
if path_info.startswith(prefix):
break
else:
header = HTTP_HEADERS[int(report_only)]
if header in existing_headers: # don't overwrite
continue
headers[header].append(csp)

for header, policies in headers.items():
response[header] = '; '.join(policies)
return response

def build_policy(self, request, response):
config = getattr(response, '_csp_config', None)
update = getattr(response, '_csp_update', None)
replace = getattr(response, '_csp_replace', None)
nonce = getattr(request, '_csp_nonce', None)
return build_policy(config=config, update=update, replace=replace,
nonce=nonce)
build_kwargs = {
key: getattr(response, '_csp_%s' % key, None)
for key in ('config', 'update', 'replace', 'select')
}
return build_policy(
nonce=getattr(request, '_csp_nonce', None),
**build_kwargs,
)
11 changes: 8 additions & 3 deletions csp/tests/settings.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import django


CSP_REPORT_ONLY = False

CSP_INCLUDE_NONCE_IN = ['default-src']
CSP_POLICY_DEFINITIONS = {
'default': {
'report_only': False,
},
'report': {
'report_only': True,
},
}

DATABASES = {
'default': {
Expand Down
Loading