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 all 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
8 changes: 8 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ 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.
- Fix CSP_EXCLUDE_URL_PREFIXES: previously this would error unless it was a
single string. NOTE: Like other settings, CSP_EXCLUDE_URL_PREFIXES is now
configured per policy in CSP_POLICY_DEFINITIONS under the key
exclude_url_prefixes.
- The block-all-mixed-content directive has been deprecated as it's now obsolete.

3.7
===
Expand Down
57 changes: 57 additions & 0 deletions csp/conf/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
__all__ = [
'defaults',
'deprecation',
'directive_to_setting',
'get_declared_policies',
'get_declared_policy_definitions',
'setting_to_directive',
'DIRECTIVES',
]

from django.conf import settings

from . import defaults
from .deprecation import (
directive_to_setting,
setting_to_directive,
_handle_legacy_settings,
)


DIRECTIVES = defaults.DIRECTIVES
PSEUDO_DIRECTIVES = defaults.PSEUDO_DIRECTIVES


def _csp_definitions_update(csp_definitions, other):
""" Update one csp definitions dictionary with another """
if isinstance(other, dict):
other = other.items()
for name, csp in other:
csp_definitions.setdefault(name, {}).update(csp)
return csp_definitions


def get_declared_policy_definitions():
custom_definitions = _csp_definitions_update(
{},
getattr(
settings,
'CSP_POLICY_DEFINITIONS',
{'default': {}},
),
)
_handle_legacy_settings(
custom_definitions['default'],
allow_legacy=not hasattr(settings, 'CSP_POLICY_DEFINITIONS'),
)
definitions = _csp_definitions_update(
{},
{name: defaults.POLICY for name in custom_definitions}
)
for name, csp in custom_definitions.items():
definitions.setdefault(name, {}).update(csp)
return definitions


def get_declared_policies():
return getattr(settings, 'CSP_POLICIES', defaults.POLICIES)
47 changes: 47 additions & 0 deletions csp/conf/defaults.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
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, # Obsolete
# Pseudo Directives
'report_only': False,
'include_nonce_in': ('default-src',),
'exclude_url_prefixes': (),
}

DIRECTIVES = set(POLICY)
PSEUDO_DIRECTIVES = {d for d in DIRECTIVES if '_' in d}
70 changes: 70 additions & 0 deletions csp/conf/deprecation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import warnings

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

from . import defaults


BLOCK_ALL_MIXED_CONTENT_DEPRECATION_WARNING = (
"block-all-mixed-content is obsolete. "
"All mixed content is now blocked if it can't be autoupgraded."
)

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


def setting_to_directive(setting, value, prefix='CSP_'):
setting = setting[len(prefix):].lower()
if setting not in defaults.PSEUDO_DIRECTIVES:
setting = setting.replace('_', '-')
assert setting in defaults.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_SETTINGS = {
directive_to_setting(directive) for directive in defaults.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(
"Setting CSP_POLICY_DEFINITIONS is not allowed with the 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)
13 changes: 5 additions & 8 deletions csp/contrib/rate_limiting.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,20 @@
from django.conf import settings

from csp.middleware import CSPMiddleware
from csp.utils import build_policy


class RateLimitedCSPMiddleware(CSPMiddleware):
"""A CSP middleware that rate-limits the number of violation reports sent
to report-uri by excluding it from some requests."""

def build_policy(self, request, response):
config = getattr(response, '_csp_config', None)
update = getattr(response, '_csp_update', None)
replace = getattr(response, '_csp_replace', {})
nonce = getattr(request, '_csp_nonce', None)
def get_build_kwargs(self, request, response):
build_kwargs = super().get_build_kwargs(request, response)
replace = build_kwargs['replace'] or {}

report_percentage = getattr(settings, 'CSP_REPORT_PERCENTAGE')
include_report_uri = random.random() < report_percentage
if not include_report_uri:
replace['report-uri'] = None
build_kwargs['replace'] = replace

return build_policy(config=config, update=update, replace=replace,
nonce=nonce)
return build_kwargs
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
Loading