Skip to content

Commit

Permalink
Add constants for CSP keywords
Browse files Browse the repository at this point in the history
This helps avoid potential errors introduced by incorrectly quoting CSP keywords.
  • Loading branch information
robhudson committed Jun 6, 2024
1 parent 3413de3 commit 29c175c
Show file tree
Hide file tree
Showing 7 changed files with 116 additions and 40 deletions.
10 changes: 10 additions & 0 deletions csp/constants.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,12 @@
HEADER = "Content-Security-Policy"
HEADER_REPORT_ONLY = "Content-Security-Policy-Report-Only"

NONE = "'none'"
REPORT_SAMPLE = "'report-sample'"
SELF = "'self'"
STRICT_DYNAMIC = "'strict-dynamic'"
UNSAFE_ALLOW_REDIRECTS = "'unsafe-allow-redirects'"
UNSAFE_EVAL = "'unsafe-eval'"
UNSAFE_HASHES = "'unsafe-hashes'"
UNSAFE_INLINE = "'unsafe-inline'"
WASM_UNSAFE_EVAL = "'wasm-unsafe-eval'"
6 changes: 3 additions & 3 deletions csp/tests/test_middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from django.test import RequestFactory
from django.test.utils import override_settings

from csp.constants import HEADER, HEADER_REPORT_ONLY
from csp.constants import HEADER, HEADER_REPORT_ONLY, SELF
from csp.middleware import CSPMiddleware
from csp.tests.utils import response

Expand All @@ -23,7 +23,7 @@ def test_add_header():

@override_settings(
CONTENT_SECURITY_POLICY={"DIRECTIVES": {"default-src": ["example.com"]}},
CONTENT_SECURITY_POLICY_REPORT_ONLY={"DIRECTIVES": {"default-src": ["'self'"]}},
CONTENT_SECURITY_POLICY_REPORT_ONLY={"DIRECTIVES": {"default-src": [SELF]}},
)
def test_both_headers():
request = rf.get("/")
Expand Down Expand Up @@ -51,7 +51,7 @@ def text_exclude():

@override_settings(
CONTENT_SECURITY_POLICY=None,
CONTENT_SECURITY_POLICY_REPORT_ONLY={"DIRECTIVES": {"default-src": ["'self'"]}},
CONTENT_SECURITY_POLICY_REPORT_ONLY={"DIRECTIVES": {"default-src": [SELF]}},
)
def test_report_only():
request = rf.get("/")
Expand Down
3 changes: 2 additions & 1 deletion csp/tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from django.test.utils import override_settings
from django.utils.functional import lazy

from csp.constants import NONE, SELF
from csp.utils import build_policy, default_config, DEFAULT_DIRECTIVES


Expand Down Expand Up @@ -182,7 +183,7 @@ def test_replace_missing_setting():


def test_config():
policy = build_policy(config={"default-src": ["'none'"], "img-src": ["'self'"]})
policy = build_policy(config={"default-src": [NONE], "img-src": [SELF]})
policy_eq("default-src 'none'; img-src 'self'", policy)


Expand Down
3 changes: 2 additions & 1 deletion csp/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@
from django.conf import settings
from django.utils.encoding import force_str

from csp.constants import SELF

DEFAULT_DIRECTIVES = {
# Fetch Directives
"child-src": None,
"connect-src": None,
"default-src": ["'self'"],
"default-src": [SELF],
"script-src": None,
"script-src-attr": None,
"script-src-elem": None,
Expand Down
52 changes: 38 additions & 14 deletions docs/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -43,31 +43,54 @@ a more slightly strict policy and is used to test the policy without breaking th

.. code-block:: python
from csp.constants import NONE, SELF
CONTENT_SECURITY_POLICY = {
"EXCLUDE_URL_PREFIXES": ["/excluded-path/"],
"DIRECTIVES": {
"default-src": ["'self'", "cdn.example.net"],
"frame-ancestors": ["'self'"],
"form-action": ["'self'"],
"default-src": [SELF, "cdn.example.net"],
"frame-ancestors": [SELF],
"form-action": [SELF],
"report-uri": "/csp-report/",
},
}
CONTENT_SECURITY_POLICY_REPORT_ONLY = {
"EXCLUDE_URL_PREFIXES": ["/excluded-path/"],
"DIRECTIVES": {
"default-src": ["'none'"],
"connect-src": ["'self'"],
"img-src": ["'self'"],
"form-action": ["'self'"],
"frame-ancestors": ["'self'"],
"script-src": ["'self'"],
"style-src": ["'self'"],
"default-src": [NONE],
"connect-src": [SELF],
"img-src": [SELF],
"form-action": [SELF],
"frame-ancestors": [SELF],
"script-src": [SELF],
"style-src": [SELF],
"upgrade-insecure-requests": True,
"report-uri": "/csp-report/",
},
}
.. note::

In the above example, the constant ``NONE`` is converted to the CSP keyword ``"'none'"`` and
is distinct from Python's ``None`` value. The CSP keyword ``'none'`` is a special value that
signifies that you do not want any sources for this directive. The ``None`` value is a
Python keyword that represents the absence of a value and when used as the value of a directive,
it will remove the directive from the policy.

This is useful when using the ``@csp_replace`` decorator to effectively clear a directive from
the base configuration as defined in the settings. For example, if the Django settings the
``frame-ancestors`` directive is set to a list of sources and you want to remove the
``frame-ancestors`` directive from the policy for this view:

.. code-block:: python
from csp.decorators import csp_replace
@csp_replace({"frame-ancestors": None})
def my_view(request): ...
Policy Settings
===============
Expand All @@ -86,8 +109,6 @@ policy.
Scripting flaw on, e.g., ``excluded-page/`` can therefore be leveraged to access everything
on the same origin.

# TODO: I can't find any documentation on the above warning.

``REPORT_PERCENTAGE``
Percentage of requests that should see the ``report-uri`` directive.
Use this to throttle the number of CSP violation reports made to your
Expand All @@ -101,8 +122,11 @@ policy.

.. note::
The "special" source values of ``'self'``, ``'unsafe-inline'``, ``'unsafe-eval'``,
``'none'`` and hash-source (``'sha256-...'``) must be quoted!
e.g.: ``"default-src": ["'self'"]``. Without quotes they will not work as intended.
``'strict-dynamic'``, ``'none'``, etc. must be quoted! e.g.: ``"default-src": ["'self'"]``.
Without quotes they will not work as intended.

Consider using the ``csp.constants`` module to get these values to help avoiding quoting
errors or typos, e.g., ``from csp.constants import SELF, STRICT_DYNAMIC``.

.. note::
Deprecated features of CSP in general have been moved to the bottom of this list.
Expand Down
12 changes: 9 additions & 3 deletions docs/contributing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,22 @@ Patches fixing bugs should include regression tests (ideally tests that
fail without the rest of the patch). Patches adding new features should
test those features thoroughly.

To run the tests, install the requirements (probably into a virtualenv_)::
To run the tests, install the requirements (probably into a virtualenv_):

.. code-block:: bash
pip install -e .
pip install -e ".[tests]"
Then just `pytest`_ to run the tests::
Then just `pytest`_ to run the tests:

.. code-block:: bash
pytest
To run the tests with coverage and get a report, use the following command::
To run the tests with coverage and get a report, use the following command:

.. code-block:: bash
pytest --cov=csp --cov-config=.coveragerc
Expand Down
70 changes: 52 additions & 18 deletions docs/decorators.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,29 +7,35 @@ Modifying the Policy with Decorators
Content Security Policies should be restricted and paranoid by default. You may, on some views,
need to expand or change the policy. django-csp includes four decorators to help.

All decorators take an optional keyword argument, ``REPORT_ONLY``, which defaults to ``False``. If
set to ``True``, the decorator will update the report-only policy instead of the enforced policy.

``@csp_exempt``
===============

Using the ``@csp_exempt`` decorator disables the CSP header on a given
view.

::
.. code-block:: python
from csp.decorators import csp_exempt
# Will not have a CSP header.
@csp_exempt()
def myview(request):
return render(...)
# Will not have a CSP report-only header.
@csp_exempt(REPORT_ONLY=True)
def myview(request):
return render(...)
You can manually set this on a per-response basis by setting the ``_csp_exempt``
or ``_csp_exempt_ro`` attribute on the response to ``True``::
or ``_csp_exempt_ro`` attribute on the response to ``True``:

.. code-block:: python
# Also will not have a CSP header.
def myview(request):
Expand All @@ -45,6 +51,7 @@ The ``@csp_update`` header allows you to **append** values to the source lists s
settings. If there is no setting, the value passed to the decorator will be used verbatim.

.. note::

To quote the CSP spec: "There's no inheritance; ... the default list is not used for that
resource type" if it is set. E.g., the following will not allow images from 'self'::

Expand All @@ -55,15 +62,17 @@ decorator excpects a single dictionary argument, where the keys are the directiv
are either strings, lists or tuples. An optional argument, ``REPORT_ONLY``, can be set to ``True``
to update the report-only policy instead of the enforced policy.

::
.. code-block:: python
from csp.decorators import csp_update
# Will append imgsrv.com to the list of values for `img-src` in the enforced policy.
@csp_update({"img-src": "imgsrv.com"})
def myview(request):
return render(...)
# Will append cdn-img.com to the list of values for `img-src` in the report-only policy.
@csp_update({"img-src": "cdn-img.com"}, REPORT_ONLY=True)
def myview(request):
Expand All @@ -77,41 +86,66 @@ The ``@csp_replace`` decorator allows you to **replace** a source list specified
there is no setting, the value passed to the decorator will be used verbatim. (See the note under
``@csp_update``.) If the specified value is None, the corresponding key will not be included.

The arguments and values are the same as ``@csp_update``::
The arguments and values are the same as ``@csp_update``:

.. code-block:: python
from csp.decorators import csp_replace
# Will allow images only from imgsrv2.com in the enforced policy.
@csp_replace({"img-src": "imgsrv2.com"})
def myview(request):
return render(...)
# Will allow images only from cdn-img2.com in the report-only policy.
@csp_replace({"img-src": "imgsrv2.com"})
def myview(request):
return render(...)
The ``csp_replace`` decorator can also be used to remove a directive from the policy by setting the
value to ``None``. For example, if the ``frame-ancestors`` directive is set in the Django settings
and you want to remove the ``frame-ancestors`` directive from the policy for this view:

.. code-block:: python
from csp.decorators import csp_replace
@csp_replace({"frame-ancestors": None})
def myview(request):
return render(...)
``@csp``
========

If you need to set the entire policy on a view, ignoring all the settings, you can use the ``@csp``
decorator. This, and the other decorators, can be stacked to update both policies if both are in
use, as shown below. The arguments and values are as above::
decorator. This can be stacked to update both the enforced policy and the report-only policy if both
are in use, as shown below.

.. code-block:: python
from csp.constants import SELF, UNSAFE_INLINE
from csp.decorators import csp
@csp({
"default_src": ["'self'"],
"img-src": ["imgsrv.com"],
"script-src": ["scriptsrv.com", "googleanalytics.com", "'unsafe-inline'"]}
})
@csp({
"default_src": ["'self'"],
"img-src": ["imgsrv.com"],
"script-src": ["scriptsrv.com", "googleanalytics.com"]},
"frame-src": ["'self'"],
REPORT_ONLY=True
})
@csp(
{
"default_src": [SELF],
"img-src": ["imgsrv.com"],
"script-src": ["scriptsrv.com", "googleanalytics.com", UNSAFE_INLINE],
}
)
@csp(
{
"default_src": [SELF],
"img-src": ["imgsrv.com"],
"script-src": ["scriptsrv.com", "googleanalytics.com"],
"frame-src": [SELF],
},
REPORT_ONLY=True,
)
def myview(request):
return render(...)

0 comments on commit 29c175c

Please sign in to comment.