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 constants for CSP keywords #222

Merged
merged 1 commit into from
Jun 6, 2024
Merged
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
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(...)