From 405cfb7f61d67d22e3867fceef45fc2740e93e76 Mon Sep 17 00:00:00 2001 From: Rob Hudson Date: Tue, 21 May 2024 16:02:20 -0700 Subject: [PATCH] Add a Django check for the old settings and update docs. --- .gitignore | 1 + csp/apps.py | 11 ++++ csp/checks.py | 81 ++++++++++++++++++++++++ csp/tests/test_checks.py | 52 +++++++++++++++ docs/configuration.rst | 13 ++-- docs/index.rst | 1 + docs/installation.rst | 25 +++++--- docs/migration-guide.rst | 133 ++++++++++++++++++++++++--------------- 8 files changed, 251 insertions(+), 66 deletions(-) create mode 100644 csp/apps.py create mode 100644 csp/checks.py create mode 100644 csp/tests/test_checks.py diff --git a/.gitignore b/.gitignore index 90f56c0..63d548a 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ .tox dist build +docs/_build diff --git a/csp/apps.py b/csp/apps.py new file mode 100644 index 0000000..caf9f45 --- /dev/null +++ b/csp/apps.py @@ -0,0 +1,11 @@ +from django.apps import AppConfig +from django.core import checks + +from csp.checks import check_django_csp_lt_4_0 + + +class CspConfig(AppConfig): + name = "csp" + + def ready(self): + checks.register(check_django_csp_lt_4_0, checks.Tags.security) diff --git a/csp/checks.py b/csp/checks.py new file mode 100644 index 0000000..96923c8 --- /dev/null +++ b/csp/checks.py @@ -0,0 +1,81 @@ +import json + +from django.conf import settings +from django.core.checks import Error + + +OUTDATED_SETTINGS = [ + "CSP_CHILD_SRC", + "CSP_CONNECT_SRC", + "CSP_DEFAULT_SRC", + "CSP_SCRIPT_SRC", + "CSP_SCRIPT_SRC_ATTR", + "CSP_SCRIPT_SRC_ELEM", + "CSP_OBJECT_SRC", + "CSP_STYLE_SRC", + "CSP_STYLE_SRC_ATTR", + "CSP_STYLE_SRC_ELEM", + "CSP_FONT_SRC", + "CSP_FRAME_SRC", + "CSP_IMG_SRC", + "CSP_MANIFEST_SRC", + "CSP_MEDIA_SRC", + "CSP_PREFETCH_SRC", + "CSP_WORKER_SRC", + "CSP_BASE_URI", + "CSP_PLUGIN_TYPES", + "CSP_SANDBOX", + "CSP_FORM_ACTION", + "CSP_FRAME_ANCESTORS", + "CSP_NAVIGATE_TO", + "CSP_REQUIRE_SRI_FOR", + "CSP_REQUIRE_TRUSTED_TYPES_FOR", + "CSP_TRUSTED_TYPES", + "CSP_UPGRADE_INSECURE_REQUESTS", + "CSP_BLOCK_ALL_MIXED_CONTENT", + "CSP_REPORT_URI", + "CSP_REPORT_TO", + "CSP_INCLUDE_NONCE_IN", +] + + +def migrate_settings(): + # This function is used to migrate settings from the old format to the new format. + config = { + "DIRECTIVES": {}, + } + REPORT_ONLY = False + + if hasattr(settings, "CSP_REPORT_ONLY"): + REPORT_ONLY = settings.CSP_REPORT_ONLY + + if hasattr(settings, "CSP_EXCLUDE_URL_PREFIXES"): + config["EXCLUDE_URL_PREFIXES"] = settings.CSP_EXCLUDE_URL_PREFIXES + + if hasattr(settings, "CSP_REPORT_PERCENTAGE"): + config["REPORT_PERCENTAGE"] = round(settings.CSP_REPORT_PERCENTAGE * 100) + + for setting in OUTDATED_SETTINGS: + if hasattr(settings, setting): + directive = setting[4:].replace("_", "-").lower() + value = getattr(settings, setting) + if value: + config["DIRECTIVES"][directive] = value + + return config, REPORT_ONLY + + +def check_django_csp_lt_4_0(app_configs, **kwargs): + check_settings = OUTDATED_SETTINGS + ["CSP_REPORT_ONLY", "CSP_EXCLUDE_URL_PREFIXES", "CSP_REPORT_PERCENTAGE"] + if any(hasattr(settings, setting) for setting in check_settings): + # Try to build the new config. + config, REPORT_ONLY = migrate_settings() + warning = ( + "You are using django-csp < 4.0 settings. Please update your settings to use the new format.\n" + "See https://django-csp.readthedocs.io/en/latest/migration-guide.html for more information.\n\n" + "We have attempted to build the new CSP config for you based on your current settings:\n\n" + f"CONTENT_SECURITY_POLICY{'_REPORT_ONLY' if REPORT_ONLY else ''} = " + json.dumps(config, indent=4, default=repr) + ) + return [Error(warning, id="csp.E001")] + + return [] diff --git a/csp/tests/test_checks.py b/csp/tests/test_checks.py new file mode 100644 index 0000000..b3f6fb0 --- /dev/null +++ b/csp/tests/test_checks.py @@ -0,0 +1,52 @@ +from django.test.utils import override_settings + +from csp.checks import check_django_csp_lt_4_0, migrate_settings + + +@override_settings( + CSP_REPORT_PERCENTAGE=0.25, + CSP_EXCLUDE_URL_PREFIXES=["/admin/"], + CSP_REPORT_ONLY=False, + CSP_DEFAULT_SRC=["'self'", "example.com"], +) +def test_migrate_settings(): + config, report_only = migrate_settings() + assert config == { + "REPORT_PERCENTAGE": 25, + "EXCLUDE_URL_PREFIXES": ["/admin/"], + "DIRECTIVES": {"default-src": ["'self'", "example.com"]}, + } + assert report_only is False + + +@override_settings( + CSP_REPORT_ONLY=True, + CSP_DEFAULT_SRC=["'self'", "example.com"], + CSP_SCRIPT_SRC=["'self'", "example.com", "'unsafe-inline'"], + CSP_INCLUDE_NONCE_IN=["script-src"], +) +def test_migrate_settings_report_only(): + config, report_only = migrate_settings() + assert config == { + "DIRECTIVES": { + "default-src": ["'self'", "example.com"], + "script-src": ["'self'", "example.com", "'unsafe-inline'"], + "include-nonce-in": ["script-src"], + } + } + assert report_only is True + + +@override_settings( + CSP_DEFAULT_SRC=["'self'", "example.com"], +) +def test_check_django_csp_lt_4_0(): + errors = check_django_csp_lt_4_0(None) + assert len(errors) == 1 + error = errors[0] + assert error.id == "csp.E001" + assert "update your settings to use the new format" in error.msg + + +def test_check_django_csp_lt_4_0_no_config(): + assert check_django_csp_lt_4_0(None) == [] diff --git a/docs/configuration.rst b/docs/configuration.rst index ce34f23..ddb77f7 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -15,15 +15,14 @@ before configuring django-csp. policies and even errors when mistakenly configuring them as a ``string``. -============================ -Migrating from django-csp v3 -============================ +Migrating from django-csp <= 3.8 +================================ -Version 4.x of django-csp introduces a new configuration format that breaks compatibility with -previous versions. If you are migrating from django-csp v3, you will need to update your settings -to the new format. See the :ref:`migration guide ` for more information. +Version 4.0 of django-csp introduces a new configuration format that breaks compatibility with +previous versions. If you are migrating from django-csp 3.8 or lower, you will need to update your +settings to the new format. See the :ref:`migration guide ` for more +information. -============= Configuration ============= diff --git a/docs/index.rst b/docs/index.rst index 5597cfb..419ab76 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -21,6 +21,7 @@ Contents: installation configuration + migration-guide decorators nonce trusted_types diff --git a/docs/installation.rst b/docs/installation.rst index 7745419..b7a15b0 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -4,26 +4,33 @@ Installing django-csp ===================== -First, install django-csp via pip or from source:: +First, install django-csp via pip or from source: - # pip - $ pip install django-csp +.. code-block:: bash -:: + pip install django-csp - # source - $ git clone https://github.com/mozilla/django-csp.git - $ cd django-csp - $ python setup.py install +Add the csp app to your ``INSTALLED_APPS`` in your project's ``settings`` module: + +.. code-block:: python + + INSTALLED_APPS = ( + # ... + 'csp', + # ... + ) Now edit your project's ``settings`` module, to add the django-csp middleware -to ``MIDDLEWARE``, like so:: +to ``MIDDLEWARE``, like so: + +.. code-block:: python MIDDLEWARE = ( # ... 'csp.middleware.CSPMiddleware', # ... ) + Note: Middleware order does not matter unless you have other middleware modifying the CSP header. That should do it! Go on to :ref:`configuring CSP `. diff --git a/docs/migration-guide.rst b/docs/migration-guide.rst index 47cef0d..442539e 100644 --- a/docs/migration-guide.rst +++ b/docs/migration-guide.rst @@ -1,78 +1,111 @@ +.. _migration-guide-chapter: -# django-csp v4 Migration Guide +============================== +django-csp 4.0 Migration Guide +============================== -## Overview +Overview +======== In the latest version of `django-csp`, the format for configuring Content Security Policy (CSP) settings has been updated are are backwards-incompatible with prior versions. The previous approach -of using individual settings prefixed with `CSP_` for each directive is no longer valid. Instead, -all CSP settings are now consolidated into one of two dict-based settings: `CONTENT_SECURITY_POLICY` -or `CONTENT_SECURITY_POLICY_REPORT_ONLY`. +of using individual settings prefixed with ``CSP_`` for each directive is no longer valid. Instead, +all CSP settings are now consolidated into one of two dict-based settings: ``CONTENT_SECURITY_POLICY`` +or ``CONTENT_SECURITY_POLICY_REPORT_ONLY``. -## Migrating from the Old Settings Format +Migrating from the Old Settings Format +====================================== -### Step 1: Identify Existing CSP Settings +Step 1: Update `django-csp` Version +----------------------------------- -First, locate all the existing CSP settings in your Django project. These settings typically start -with the `CSP_` prefix, such as `CSP_DEFAULT_SRC`, `CSP_SCRIPT_SRC`, `CSP_IMG_SRC`, and -`CSP_EXCLUDE_URL_PREFIXES`. +First, update the `django-csp` package to the latest version that supports the new settings format. +You can do this by running: -### Step 2: Create the New Settings Dictionary +.. code-block:: bash -In your Django project's `settings.py` file, create a new dictionary called -`CONTENT_SECURITY_POLICY` or `CONTENT_SECURITY_POLICY_REPORT_ONLY`, depending on whether you want to -enforce the policy or report violations. + pip install -U django-csp + +Step 2: Add the `csp` app to `INSTALLED_APPS` +--------------------------------------------- + +In your Django project's `settings.py` file, add the `csp` app to the ``INSTALLED_APPS`` setting: + +.. code-block:: python + + INSTALLED_APPS = [ + ... + "csp", + ... + ] + +Step 3: Run the Django check command +------------------------------------ + +Run the Django check command to get a settings config based on your existing CSP settings: + +.. code-block:: bash + + python manage.py check + +This can help you identify the existing CSP settings in your project and provide a starting point +for migrating to the new format. -```python -CONTENT_SECURITY_POLICY = { - "EXCLUDE_URL_PREFIXES": [], - "DIRECTIVES": {}, -} -``` +Step 4: Identify Existing CSP Settings +-------------------------------------- -### Step 3: Migrate Existing Settings +Locate all the existing CSP settings in your Django project. These settings typically start with the +``CSP_`` prefix, such as ``CSP_DEFAULT_SRC``, ``CSP_SCRIPT_SRC``, ``CSP_IMG_SRC``, etc. -Migrate your existing CSP settings to the new format by populating the `DIRECTIVES` dictionary -inside the `CONTENT_SECURITY_POLICY` setting. The keys of the `DIRECTIVES` dictionary should be the +Step 5: Create the New Settings Dictionary +------------------------------------------ + +In your Django project's `settings.py` file, create a new dictionary called +``CONTENT_SECURITY_POLICY`` or ``CONTENT_SECURITY_POLICY_REPORT_ONLY``, depending on whether you want to +enforce the policy or only report violations. Use the output from the Django check command as a +starting point to populate this dictionary. + +Step 6: Migrate Existing Settings +--------------------------------- + +Migrate your existing CSP settings to the new format by populating the ``DIRECTIVES`` dictionary +inside the ``CONTENT_SECURITY_POLICY`` setting. The keys of the ``DIRECTIVES`` dictionary should be the CSP directive names in lowercase, and the values should be lists containing the corresponding -sources. +sources. The Django check command output can help you identify the directive names and sources. For example, if you had the following old settings: -```python -CSP_DEFAULT_SRC = ["'self'", "*.example.com"] -CSP_SCRIPT_SRC = ["'self'", "js.cdn.com/example/"] -CSP_IMG_SRC = ["'self'", "data:", "example.com"] -CSP_EXCLUDE_URL_PREFIXES = ["/admin"] -``` +.. code-block:: python + + CSP_DEFAULT_SRC = ["'self'", "*.example.com"] + CSP_SCRIPT_SRC = ["'self'", "js.cdn.com/example/"] + CSP_IMG_SRC = ["'self'", "data:", "example.com"] + CSP_EXCLUDE_URL_PREFIXES = ["/admin"] The new settings would be: -```python -CONTENT_SECURITY_POLICY = { - "EXCLUDE_URL_PREFIXES": ["/admin"], - "DIRECTIVES": { - "default-src": ["'self'", "*.example.com"], - "script-src": ["'self'", "js.cdn.com/example/"], - "img-src": ["'self'", "data:", "example.com"], - }, -} -``` - -Note that the directive names in the `DIRECTIVES` dictionary are in lowercase and use dashes instead -of underscores. +.. code-block:: python -### Step 4: Remove Old Settings + CONTENT_SECURITY_POLICY = { + "EXCLUDE_URL_PREFIXES": ["/admin"], + "DIRECTIVES": { + "default-src": ["'self'", "*.example.com"], + "script-src": ["'self'", "js.cdn.com/example/"], + "img-src": ["'self'", "data:", "example.com"], + }, + } -After migrating to the new settings format, remove all the old `CSP_` prefixed settings from your -`settings.py` file. +Note that the directive names in the ``DIRECTIVES`` dictionary are in lowercase and use dashes instead +of underscores. -### Step 5: Update the django-csp Version +Step 7: Remove Old Settings +--------------------------- -Finally, update your `django-csp` version to the latest version that support the new settings -format. +After migrating to the new settings format, remove all the old ``CSP_`` prefixed settings from your +`settings.py` file. -## Conclusion +Conclusion +========== By following this migration guide, you should be able to successfully update your Django project to use the new dict-based CSP settings format introduced in the latest version of `django-csp`. This