Skip to content

Commit

Permalink
Add a Django check for the old settings and update docs.
Browse files Browse the repository at this point in the history
  • Loading branch information
robhudson committed May 21, 2024
1 parent e6204b6 commit 405cfb7
Show file tree
Hide file tree
Showing 8 changed files with 251 additions and 66 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@
.tox
dist
build
docs/_build
11 changes: 11 additions & 0 deletions csp/apps.py
Original file line number Diff line number Diff line change
@@ -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)
81 changes: 81 additions & 0 deletions csp/checks.py
Original file line number Diff line number Diff line change
@@ -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 []
52 changes: 52 additions & 0 deletions csp/tests/test_checks.py
Original file line number Diff line number Diff line change
@@ -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) == []
13 changes: 6 additions & 7 deletions docs/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <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 <migration-guide-chapter>` for more
information.

=============
Configuration
=============

Expand Down
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Contents:

installation
configuration
migration-guide
decorators
nonce
trusted_types
Expand Down
25 changes: 16 additions & 9 deletions docs/installation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <configuration-chapter>`.
133 changes: 83 additions & 50 deletions docs/migration-guide.rst
Original file line number Diff line number Diff line change
@@ -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
Expand Down

0 comments on commit 405cfb7

Please sign in to comment.