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

Add support for csp_clear decorator. #202

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ 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 csp_clear decorator to clear directives per view (#201)

3.7
===
Expand Down
10 changes: 8 additions & 2 deletions csp/contrib/rate_limiting.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class RateLimitedCSPMiddleware(CSPMiddleware):
to report-uri by excluding it from some requests."""

def build_policy(self, request, response):
clear = getattr(response, '_csp_clear', None)
config = getattr(response, '_csp_config', None)
update = getattr(response, '_csp_update', None)
replace = getattr(response, '_csp_replace', {})
Expand All @@ -21,5 +22,10 @@ def build_policy(self, request, response):
if not include_report_uri:
replace['report-uri'] = None

return build_policy(config=config, update=update, replace=replace,
nonce=nonce)
return build_policy(
clear=clear,
config=config,
update=update,
replace=replace,
nonce=nonce,
)
13 changes: 13 additions & 0 deletions csp/decorators.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
from functools import wraps


def csp_clear(*args):
clear = set(value.lower().replace('_', '-') for value in args)

def decorator(f):
@wraps(f)
def _wrapped(*a, **kw):
r = f(*a, **kw)
r._csp_clear = clear
return r
return _wrapped
return decorator


def csp_exempt(f):
@wraps(f)
def _wrapped(*a, **kw):
Expand Down
10 changes: 8 additions & 2 deletions csp/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,15 @@ def process_response(self, request, response):
return response

def build_policy(self, request, response):
clear = getattr(response, '_csp_clear', None)
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)
return build_policy(
clear=clear,
config=config,
update=update,
replace=replace,
nonce=nonce,
)
10 changes: 9 additions & 1 deletion csp/tests/test_decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from django.test import RequestFactory
from django.test.utils import override_settings

from csp.decorators import csp, csp_replace, csp_update, csp_exempt
from csp.decorators import csp, csp_replace, csp_update, csp_exempt, csp_clear
from csp.middleware import CSPMiddleware
from csp.tests.utils import response

Expand Down Expand Up @@ -110,3 +110,11 @@ def view_with_decorator(request):
mw.process_response(REQUEST, response)
policy_list = sorted(response['Content-Security-Policy'].split("; "))
assert policy_list == ["font-src bar.com", "img-src foo.com"]


def test_csp_clear():
@csp_clear("IMG_SRC", "frame-ancestors")
def view(request):
return HttpResponse()
response = view(REQUEST)
assert response._csp_clear == set(["img-src", "frame-ancestors"])
12 changes: 12 additions & 0 deletions csp/tests/test_middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,18 @@ def test_use_update():
assert response[HEADER] == "default-src 'self' example.com"


def test_use_clear():
request = rf.get('/')
response = HttpResponse()
response._csp_update = {
'default-src': ['example.com'],
'frame-ancestors': ['example.com'],
}
response._csp_clear = set(["frame-ancestors"])
mw.process_response(request, response)
assert response[HEADER] == "default-src 'self' example.com"


@override_settings(CSP_IMG_SRC=['foo.com'])
def test_use_replace():
request = rf.get('/')
Expand Down
7 changes: 6 additions & 1 deletion csp/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,15 @@ def from_settings():
}


def build_policy(config=None, update=None, replace=None, nonce=None):
def build_policy(
clear=None, config=None, update=None, replace=None, nonce=None):
"""Builds the policy as a string from the settings."""

if config is None:
config = from_settings()
# Be careful, don't mutate config as it could be from settings

clear = clear if clear is not None else set()
update = update if update is not None else {}
replace = replace if replace is not None else {}
csp = {}
Expand All @@ -84,6 +86,9 @@ def build_policy(config=None, update=None, replace=None, nonce=None):
else:
csp[k] += tuple(v)

for value in clear:
csp.pop(value, None)

report_uri = csp.pop('report-uri', None)

policy_parts = {}
Expand Down
24 changes: 24 additions & 0 deletions docs/decorators.rst
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,30 @@ The arguments and values are the same as ``@csp_update``::
return render(...)


``@csp_clear``
================

The ``@csp_clear`` decorator allows you to **clear** a CSP directive. Clearing
a directive will take priority over any of the other options.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if an admonition directive here like warning or even danger might be worthwhile.


The values to the decorator are the same as the :ref:`settings
<configuration-chapter>` without the ``CSP_`` prefix, e.g. ``IMG_SRC``.
(They are also case-insensitive, so `img-src` also works)::

from csp.decorators import csp_clear

# By default only allow responses to be served in an iframe
# from imgsrv.com
# settings.CSP_FRAME_ANCESTORS = ['imgsrv.com']
#
# Using the @csp_clear decorator to clear the `frame-ancestors` directive
# for this particular view will allow it to be served in an iframe from _any_
# domain, including imgsrv.com.
@csp_clear('frame-ancestors')
def myview(request):
return render(...)


``@csp``
========

Expand Down