From a3466478c47520a0fba91e8b4346d92661e482f1 Mon Sep 17 00:00:00 2001 From: Rob Hudson Date: Thu, 2 May 2024 09:07:05 -0700 Subject: [PATCH] Add constants for CSP keywords This helps avoid potential errors introduced by incorrectly quoting CSP keywords. --- csp/constants.py | 10 ++++++++ csp/tests/test_middleware.py | 6 ++--- csp/tests/test_utils.py | 3 ++- csp/utils.py | 3 ++- docs/configuration.rst | 47 +++++++++++++++++++++++++++--------- docs/decorators.rst | 9 ++++--- 6 files changed, 57 insertions(+), 21 deletions(-) diff --git a/csp/constants.py b/csp/constants.py index 8cf10bc..95242c3 100644 --- a/csp/constants.py +++ b/csp/constants.py @@ -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'" diff --git a/csp/tests/test_middleware.py b/csp/tests/test_middleware.py index e52971a..90a524a 100644 --- a/csp/tests/test_middleware.py +++ b/csp/tests/test_middleware.py @@ -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 @@ -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("/") @@ -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("/") diff --git a/csp/tests/test_utils.py b/csp/tests/test_utils.py index 188fa3b..041659c 100644 --- a/csp/tests/test_utils.py +++ b/csp/tests/test_utils.py @@ -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 @@ -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) diff --git a/csp/utils.py b/csp/utils.py index cf62ee1..cc645bb 100644 --- a/csp/utils.py +++ b/csp/utils.py @@ -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, diff --git a/docs/configuration.rst b/docs/configuration.rst index 834b417..c2a8d59 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -43,12 +43,14 @@ 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/", }, } @@ -56,18 +58,36 @@ a more slightly strict policy and is used to test the policy without breaking th 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, e.g. the following will remove the + ``frame-ancestors`` directive from the policy: + + .. code-block:: python + + CONTENT_SECURITY_POLICY = { + "DIRECTIVES": { + # ... + "frame-ancestors": None, + } + } + Policy Settings =============== @@ -101,8 +121,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. diff --git a/docs/decorators.rst b/docs/decorators.rst index 6c36378..d35b7d4 100644 --- a/docs/decorators.rst +++ b/docs/decorators.rst @@ -99,18 +99,19 @@ If you need to set the entire policy on a view, ignoring all the settings, you c 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:: + from csp.constants import SELF, UNSAFE_INLINE from csp.decorators import csp @csp({ - "default_src": ["'self'"], + "default_src": [SELF], "img-src": ["imgsrv.com"], - "script-src": ["scriptsrv.com", "googleanalytics.com", "'unsafe-inline'"]} + "script-src": ["scriptsrv.com", "googleanalytics.com", UNSAFE_INLINE]} }) @csp({ - "default_src": ["'self'"], + "default_src": [SELF], "img-src": ["imgsrv.com"], "script-src": ["scriptsrv.com", "googleanalytics.com"]}, - "frame-src": ["'self'"], + "frame-src": [SELF], REPORT_ONLY=True }) def myview(request):