django-csp 4.0 Migration Guide

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 supported. 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

Update django-csp

First, update the django-csp package to the latest version that supports the new settings format. You can do this by running:

pip install -U django-csp

Add the csp app to INSTALLED_APPS

In your Django project’s settings.py file, add the csp app to the INSTALLED_APPS setting:

INSTALLED_APPS = [
    # ...
    "csp",
    # ...
]

Run the Django check command

This is optional but can help kick start the new settings configuration for you. Run the Django check command which will look for old settings and output a configuration in the new format:

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.

Identify Existing CSP Settings

Locate all the existing CSP settings in your Django project. These settings start with the CSP_ prefix, such as CSP_DEFAULT_SRC, CSP_SCRIPT_SRC, CSP_IMG_SRC, etc.

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, or both. Use the output from the Django check command as a starting point to populate this dictionary.

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. The Django check command output can help you identify the directive names and sources.

For example, if you had the following old settings:

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:

from csp.constants import SELF

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

The keys in the DIRECTIVES dictionary, the directive names, are in lowercase and use dashes instead of underscores to match the CSP specification.

Note

If you were using the CSP_INCLUDE_NONCE_IN setting, this has been removed in the new settings format.

Previously: You could use the CSP_INCLUDE_NONCE_IN setting to specify which directives in your Content Security Policy (CSP) should include a nonce.

Now: You can include a nonce in any directive by adding the NONCE constant from the csp.constants module to the list of sources for that directive.

For example, if you had CSP_INCLUDE_NONCE_IN = ["script-src"], this should be updated to include the NONCE sentinel in the script-src directive values:

from csp.constants import NONCE, SELF

CONTENT_SECURITY_POLICY = {
    "DIRECTIVES": {
        "script-src": [SELF, NONCE],
        # ...
    },
}

Note

If you were using the CSP_REPORT_PERCENTAGE setting, this should be updated to be a float percentage between 0.0 and 100.0. For example, if you had CSP_REPORT_PERCENTAGE = 0.1, this should be updated to 10.0 to represent 10% of CSP errors will be reported:

CONTENT_SECURITY_POLICY = {
    "REPORT_PERCENTAGE": 10.0,
    "DIRECTIVES": {
        "report-uri": "/csp-report/",
        # ...
    },
}

Remove Old Settings

After migrating to the new settings format, remove all the old CSP_ prefixed settings from your settings.py file.

Update the CSP decorators

If you are using the CSP decorators in your views, those will need to be updated as well. The decorators now accept a dictionary containing the CSP directives as an argument. For example:

from csp.decorators import csp_update


@csp_update({"default-src": ["another-url.com"]})
def my_view(request): ...

Additionally, each decorator now takes an optional REPORT_ONLY argument to specify whether the policy should be enforced or only report violations. For example:

from csp.constants import SELF
from csp.decorators import csp


@csp({"default-src": [SELF]}, REPORT_ONLY=True)
def my_view(request): ...

Due to the addition of the REPORT_ONLY argument and for consistency, the csp_exempt decorator now requires parentheses when used with and without arguments. For example:

from csp.decorators import csp_exempt


@csp_exempt()
@csp_exempt(REPORT_ONLY=True)
def my_view(request): ...

Look for uses of the following decorators in your code: @csp, @csp_update, @csp_replace, and @csp_exempt.

Migrating Custom Middleware

The CSPMiddleware has changed in order to support easier extension via subclassing.

The CSPMiddleware.build_policy and CSPMiddleware.build_policy_ro methods have been deprecated in 4.0 and replaced with a new method CSPMiddleware.build_policy_parts.

Note

The deprecated methods will be removed in 4.1.

Unlike the old methods, which returned the built CSP policy header string, build_policy_parts returns a dataclass that can be modified and updated before the policy is built. This allows custom middleware to modify the policy whilst inheriting behaviour from the base classes.

An existing custom middleware, such as this:

from django.http import HttpRequest, HttpResponseBase

from csp.middleware import CSPMiddleware, PolicyParts


class ACustomMiddleware(CSPMiddleware):

    def build_policy(self, request: HttpRequest, response: HttpResponseBase) -> str:
        config = getattr(response, "_csp_config", None)
        update = getattr(response, "_csp_update", None)
        replace = getattr(response, "_csp_replace", {})
        nonce = getattr(request, "_csp_nonce", None)

        # ... do custom CSP policy logic ...

        return build_policy(config=config, update=update, replace=replace, nonce=nonce)

    def build_policy_ro(self, request: HttpRequest, response: HttpResponseBase) -> str:
        config = getattr(response, "_csp_config_ro", None)
        update = getattr(response, "_csp_update_ro", None)
        replace = getattr(response, "_csp_replace_ro", {})
        nonce = getattr(request, "_csp_nonce", None)

        # ... do custom CSP report-only policy logic ...

        return build_policy(config=config, update=update, replace=replace, nonce=nonce)

can be replaced with this:

from django.http import HttpRequest, HttpResponseBase

from csp.middleware import CSPMiddleware, PolicyParts


class ACustomMiddleware(CSPMiddleware):

    def get_policy_parts(
        self,
        request: HttpRequest,
        response: HttpResponseBase,
        report_only: bool = False,
    ) -> PolicyParts:
        policy_parts = super().get_policy_parts(request, response, report_only)

        if report_only:
            ...  # do custom CSP report-only policy logic
        else:
            ...  # do custom CSP policy logic

        return policy_parts

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 change aligns the package with the latest CSP specification and provides a more organized and flexible way to configure your Content Security Policy.