diff options
| author | Rob Hudson <rob@cogit8.org> | 2025-05-03 10:01:58 -0700 |
|---|---|---|
| committer | nessita <124304+nessita@users.noreply.github.com> | 2025-06-27 15:57:02 -0300 |
| commit | d63241ebc7067fdebbaf704989b34fcd8f26bbe9 (patch) | |
| tree | 07b5a5cb0c70c446f5f0fb9ad2834501fc3d6544 | |
| parent | 3f59711581bd22ebd0f13fb040b15b69c0eee21f (diff) | |
Fixed #15727 -- Added Content Security Policy (CSP) support.
This initial work adds a pair of settings to configure specific CSP
directives for enforcing or reporting policy violations, a new
`django.middleware.csp.ContentSecurityPolicyMiddleware` to apply the
appropriate headers to responses, and a context processor to support CSP
nonces in templates for safely inlining assets.
Relevant documentation has been added for the 6.0 release notes,
security overview, a new how-to page, and a dedicated reference section.
Thanks to the multiple reviewers for their precise and valuable feedback.
Co-authored-by: Natalia <124304+nessita@users.noreply.github.com>
26 files changed, 1192 insertions, 1 deletions
diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py index 672c46b88f..a414d1428c 100644 --- a/django/conf/global_settings.py +++ b/django/conf/global_settings.py @@ -663,6 +663,12 @@ SECURE_REFERRER_POLICY = "same-origin" SECURE_SSL_HOST = None SECURE_SSL_REDIRECT = False +################## +# CSP MIDDLEWARE # +################## +SECURE_CSP = {} +SECURE_CSP_REPORT_ONLY = {} + # RemovedInDjango70Warning: A transitional setting helpful in early adoption of # HTTPS as the default protocol in urlize and urlizetrunc when no protocol is # provided. Set to True to assume HTTPS during the Django 6.x release cycle. diff --git a/django/core/checks/security/base.py b/django/core/checks/security/base.py index f85adabd1a..9506052196 100644 --- a/django/core/checks/security/base.py +++ b/django/core/checks/security/base.py @@ -141,6 +141,11 @@ E024 = Error( W025 = Warning(SECRET_KEY_WARNING_MSG, id="security.W025") +E026 = Error( + "The Content Security Policy setting '%s' must be a dictionary (got %r instead).", + id="security.E026", +) + def _security_middleware(): return "django.middleware.security.SecurityMiddleware" in settings.MIDDLEWARE @@ -281,3 +286,19 @@ def check_cross_origin_opener_policy(app_configs, **kwargs): ): return [E024] return [] + + +@register(Tags.security) +def check_csp_settings(app_configs, **kwargs): + """ + Validate that CSP settings are properly configured when enabled. + + Ensures both SECURE_CSP and SECURE_CSP_REPORT_ONLY are dictionaries. + """ + # CSP settings must be a dictionary or None. + return [ + Error(E026.msg % (name, value), id=E026.id) + for name in ("SECURE_CSP", "SECURE_CSP_REPORT_ONLY") + if (value := getattr(settings, name, None)) is not None + and not isinstance(value, dict) + ] diff --git a/django/middleware/csp.py b/django/middleware/csp.py new file mode 100644 index 0000000000..e1c66ada5a --- /dev/null +++ b/django/middleware/csp.py @@ -0,0 +1,36 @@ +from http import HTTPStatus + +from django.conf import settings +from django.utils.csp import CSP, LazyNonce, build_policy +from django.utils.deprecation import MiddlewareMixin + + +def get_nonce(request): + return getattr(request, "_csp_nonce", None) + + +class ContentSecurityPolicyMiddleware(MiddlewareMixin): + def process_request(self, request): + request._csp_nonce = LazyNonce() + + def process_response(self, request, response): + # In DEBUG mode, exclude CSP headers for specific status codes that + # trigger the debug view. + exempted_status_codes = { + HTTPStatus.NOT_FOUND, + HTTPStatus.INTERNAL_SERVER_ERROR, + } + if settings.DEBUG and response.status_code in exempted_status_codes: + return response + + nonce = get_nonce(request) + for header, config in [ + (CSP.HEADER_ENFORCE, settings.SECURE_CSP), + (CSP.HEADER_REPORT_ONLY, settings.SECURE_CSP_REPORT_ONLY), + ]: + # If headers are already set on the response, don't overwrite them. + # This allows for views to set their own CSP headers as needed. + if config and header not in response: + response.headers[str(header)] = build_policy(config, nonce) + + return response diff --git a/django/template/context_processors.py b/django/template/context_processors.py index 32753032fc..f9e5f218e4 100644 --- a/django/template/context_processors.py +++ b/django/template/context_processors.py @@ -10,6 +10,7 @@ of a DjangoTemplates backend and used by RequestContext. import itertools from django.conf import settings +from django.middleware.csp import get_nonce from django.middleware.csrf import get_token from django.utils.functional import SimpleLazyObject, lazy @@ -87,3 +88,10 @@ def media(request): def request(request): return {"request": request} + + +def csp(request): + """ + Add the CSP nonce to the context. + """ + return {"csp_nonce": get_nonce(request)} diff --git a/django/utils/csp.py b/django/utils/csp.py new file mode 100644 index 0000000000..b989a47c23 --- /dev/null +++ b/django/utils/csp.py @@ -0,0 +1,110 @@ +import secrets +from enum import StrEnum + +from django.utils.functional import SimpleLazyObject, empty + + +class CSP(StrEnum): + """ + Content Security Policy constants for directive values and special tokens. + + These constants represent: + 1. Standard quoted string values from the CSP spec (e.g., 'self', 'unsafe-inline') + 2. Special placeholder tokens (NONCE) that get replaced by the middleware + + Using this enum instead of raw strings provides better type checking, + autocompletion, and protection against common mistakes like: + + - Typos (e.g., 'noone' instead of 'none') + - Missing quotes (e.g., ["self"] instead of ["'self'"]) + - Inconsistent quote styles (e.g., ["'self'", "\"unsafe-inline\""]) + + Example usage in Django settings: + + SECURE_CSP = { + "default-src": [CSP.NONE], + "script-src": [CSP.SELF, CSP.NONCE], + } + + """ + + # HTTP Headers. + HEADER_ENFORCE = "Content-Security-Policy" + HEADER_REPORT_ONLY = "Content-Security-Policy-Report-Only" + + # Standard CSP directive values. + NONE = "'none'" + REPORT_SAMPLE = "'report-sample'" + SELF = "'self'" + STRICT_DYNAMIC = "'strict-dynamic'" + UNSAFE_EVAL = "'unsafe-eval'" + UNSAFE_HASHES = "'unsafe-hashes'" + UNSAFE_INLINE = "'unsafe-inline'" + WASM_UNSAFE_EVAL = "'wasm-unsafe-eval'" + + # Special placeholder that gets replaced by the middleware. + # The value itself is arbitrary and should not be mistaken for a real nonce. + NONCE = "<CSP_NONCE_SENTINEL>" + + +class LazyNonce(SimpleLazyObject): + """ + Lazily generates a cryptographically secure nonce string, for use in CSP headers. + + The nonce is only generated when first accessed (e.g., via string + interpolation or inside a template). + + The nonce will evaluate as `True` if it has been generated, and `False` if + it has not. This is useful for third-party Django libraries that want to + support CSP without requiring it. + + Example Django template usage with context processors enabled: + + <script{% if csp_nonce %} nonce="{{ csp_nonce }}"...{% endif %}> + + The `{% if %}` block will only render if the nonce has been evaluated elsewhere. + + """ + + def __init__(self): + super().__init__(self._generate) + + def _generate(self): + return secrets.token_urlsafe(16) + + def __bool__(self): + return self._wrapped is not empty + + +def build_policy(config, nonce=None): + policy = [] + + for directive, values in config.items(): + if values in (None, False): + continue + + if values is True: + rendered_value = "" + else: + if isinstance(values, set): + # Sort values for consistency, preventing cache invalidation + # between requests and ensuring reliable browser caching. + values = sorted(values) + elif not isinstance(values, list | tuple): + values = [values] + + # Replace the nonce sentinel with the actual nonce values, if the + # sentinel is found and a nonce is provided. Otherwise, remove it. + if (has_sentinel := CSP.NONCE in values) and nonce: + values = [f"'nonce-{nonce}'" if v == CSP.NONCE else v for v in values] + elif has_sentinel: + values = [v for v in values if v != CSP.NONCE] + + if not values: + continue + + rendered_value = " ".join(values) + + policy.append(f"{directive} {rendered_value}".rstrip()) + + return "; ".join(policy) diff --git a/docs/howto/csp.txt b/docs/howto/csp.txt new file mode 100644 index 0000000000..756f815bf2 --- /dev/null +++ b/docs/howto/csp.txt @@ -0,0 +1,100 @@ +=========================================== +How to use Django's Content Security Policy +=========================================== + +.. _csp-config: + +Basic config +============ + +To enable Content Security Policy (CSP) in your Django project: + +1. Add the CSP middleware to your :setting:`MIDDLEWARE` setting:: + + MIDDLEWARE = [ + # ... + "django.middleware.csp.ContentSecurityPolicyMiddleware", + # ... + ] + +2. Configure the CSP policies in your ``settings.py`` using either + :setting:`SECURE_CSP` or :setting:`SECURE_CSP_REPORT_ONLY` (or both). The + :ref:`CSP Settings docs <csp-settings>` provide more details about the + differences between these two:: + + from django.utils.csp import CSP + + # To enforce a CSP policy: + SECURE_CSP = { + "default-src": [CSP.SELF], + # Add more directives to be enforced. + } + + # Or for report-only mode: + SECURE_CSP_REPORT_ONLY = { + "default-src": [CSP.SELF], + # Add more directives as needed. + "report-uri": "/path/to/reports-endpoint/", + } + +.. _csp-nonce-config: + +Nonce config +============ + +To use nonces in your CSP policy, beside the basic config, you need to: + +1. Include the :attr:`~django.utils.csp.CSP.NONCE` placeholder value in the CSP + settings. This only applies to ``script-src`` or ``style-src`` directives:: + + from django.utils.csp import CSP + + SECURE_CSP = { + "default-src": [CSP.SELF], + # Allow self-hosted scripts and script tags with matching `nonce` attr. + "script-src": [CSP.SELF, CSP.NONCE], + # Example of the less secure 'unsafe-inline' option. + "style-src": [CSP.SELF, CSP.UNSAFE_INLINE], + } + +2. Add the :func:`~django.template.context_processors.csp` context processor to + your :setting:`TEMPLATES` setting. This makes the generated nonce value + available in the Django templates as the ``csp_nonce`` context variable:: + + TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "OPTIONS": { + "context_processors": [ + # ... + "django.template.context_processors.csp", + ], + }, + }, + ] + +3. In your templates, add the ``nonce`` attribute to the relevant inline + ``<style>`` or ``<script>`` tags, using the ``csp_nonce`` context variable: + + .. code-block:: html+django + + <style nonce="{{ csp_nonce }}"> + /* These inline styles will be allowed. */ + </style> + + <script nonce="{{ csp_nonce }}"> + // This inline JavaScript will be allowed. + </script> + +.. admonition:: Caching and Nonce Reuse + + The :class:`~django.middleware.csp.ContentSecurityPolicyMiddleware` + automatically handles generating a unique nonce and inserting the + appropriate ``nonce-<value>`` source expression into the + ``Content-Security-Policy`` (or ``Content-Security-Policy-Report-Only``) + header when the nonce is used in a template. + + To ensure correct behavior, make sure both the HTML and the header are + generated within the same request and not served from cache. See the + reference documentation on :ref:`csp-nonce` for implementation details and + important caching considerations. diff --git a/docs/howto/index.txt b/docs/howto/index.txt index d49a9b1206..00acf5c837 100644 --- a/docs/howto/index.txt +++ b/docs/howto/index.txt @@ -57,6 +57,7 @@ Other guides :maxdepth: 1 auth-remote-user + csp csrf custom-file-storage custom-management-commands diff --git a/docs/index.txt b/docs/index.txt index 358c465df5..330e191e1c 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -251,6 +251,7 @@ applications and Django provides multiple protection tools and mechanisms: * :doc:`Cross Site Request Forgery protection <ref/csrf>` * :doc:`Cryptographic signing <topics/signing>` * :ref:`Security Middleware <security-middleware>` +* :doc:`Content Security Policy <ref/csp>` Internationalization and localization ===================================== diff --git a/docs/ref/checks.txt b/docs/ref/checks.txt index b9cb1d19cf..bb54dbdb98 100644 --- a/docs/ref/checks.txt +++ b/docs/ref/checks.txt @@ -568,6 +568,8 @@ The following checks are run if you use the :option:`check --deploy` option: ``'django-insecure-'`` indicating that it was generated automatically by Django. Please generate a long and random value, otherwise many of Django's security-critical features will be vulnerable to attack. +* **security.E026**: The CSP setting ``<SETTING_NAME>`` must be a dictionary + (got ``<value>`` instead). The following checks verify that your security-related settings are correctly configured: diff --git a/docs/ref/csp.txt b/docs/ref/csp.txt new file mode 100644 index 0000000000..e3666c9129 --- /dev/null +++ b/docs/ref/csp.txt @@ -0,0 +1,210 @@ +======================= +Content Security Policy +======================= + +.. versionadded:: 6.0 + +.. module:: django.middleware.csp + :synopsis: Middleware for Content Security Policy headers + +Content Security Policy (CSP) is a web security standard that helps prevent +content injection attacks by restricting the sources from which content can be +loaded. It plays an important role in a comprehensive :ref:`security strategy +<security-csp>`. + +For configuration instructions in a Django project, see the :ref:`Using CSP +<csp-config>` documentation. For an HTTP guide about CSP, see the `MDN Guide on +CSP <https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP>`_. + +.. _csp-overview: + +Overview +======== + +The `Content-Security-Policy specification <https://www.w3.org/TR/CSP3/>`_ +defines two complementary headers: + +* ``Content-Security-Policy``: Enforces the CSP policy, blocking content that + violates the defined directives. +* ``Content-Security-Policy-Report-Only``: Reports CSP violations without + blocking content, allowing for non-intrusive testing. + +Each policy is composed of one or more directives and their values, which +together instruct the browser on how to handle specific types of content. + +When the :class:`~django.middleware.csp.ContentSecurityPolicyMiddleware` is +enabled, Django automatically builds and attaches the appropriate headers to +each response based on the configured :ref:`settings <csp-settings>`, unless +they have already been set by another layer. + +.. _csp-settings: + +Settings +======== + +The :class:`~django.middleware.csp.ContentSecurityPolicyMiddleware` is +configured using the following settings: + +* :setting:`SECURE_CSP`: defines the **enforced Content Security Policy**. +* :setting:`SECURE_CSP_REPORT_ONLY`: defines a **report-only Content Security Policy**. + +.. admonition:: These settings can be used independently or together + + * Use :setting:`SECURE_CSP` alone to enforce a policy that has already been + tested and verified. + * Use :setting:`SECURE_CSP_REPORT_ONLY` on its own to evaluate a new policy + without disrupting site behavior. This mode does not block violations, it + only logs them. It's useful for testing and monitoring, but provides no + protection against active threats. + * Use *both* to maintain an enforced baseline while experimenting with + changes. Even for well-established policies, continuing to collect reports + reports can help detect regressions, unexpected changes in behavior, or + potential tampering in production environments. + +.. _csp-reports: + +Policy violation reports +======================== + +When a CSP violation occurs, browsers typically log details to the developer +console, providing immediate feedback during development. To also receive these +reports programmatically, the policy must include a `reporting directive +<https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy#reporting_directives>`_ +such as ``report-uri`` that specifies where violation data should be sent. + +Django supports configuring these directives via the +:setting:`SECURE_CSP_REPORT_ONLY` settings, but reports will only be issued by +the browser if the policy explicitly includes a valid reporting directive. + +Django does not provide built-in functionality to receive, store, or process +violation reports. To collect and analyze them, you must implement your own +reporting endpoint or integrate with a third-party monitoring service. + +.. _csp-constants: + +CSP constants +============= + +Django provides predefined constants representing common CSP source expression +keywords such as ``'self'``, ``'none'``, and ``'unsafe-inline'``. These +constants are intended for use in the directive values defined in the settings. + +They are available through the :class:`~django.utils.csp.CSP` enum, and using +them is recommended over raw strings. This helps avoid common mistakes such as +typos, improper quoting, or inconsistent formatting, and ensures compliance +with the CSP specification. + +.. module:: django.utils.csp + :synopsis: Constants for Content Security Policy + +.. class:: CSP + + Enum providing standardized constants for common CSP source expressions. + + .. attribute:: NONE + + Represents ``'none'``. Blocks loading resources for the given directive. + + .. attribute:: REPORT_SAMPLE + + Represents ``'report-sample'``. Instructs the browser to include a sample + of the violating code in reports. Note that this may expose sensitive + data. + + .. attribute:: SELF + + Represents ``'self'``. Allows loading resources from the same origin + (same scheme, host, and port). + + .. attribute:: STRICT_DYNAMIC + + Represents ``'strict-dynamic'``. Allows execution of scripts loaded by a + trusted script (e.g., one with a valid nonce or hash), without needing + ``'unsafe-inline'``. + + .. attribute:: UNSAFE_EVAL + + Represents ``'unsafe-eval'``. Allows use of ``eval()`` and similar + JavaScript functions. Strongly discouraged. + + .. attribute:: UNSAFE_HASHES + + Represents ``'unsafe-hashes'``. Allows inline event handlers and some + ``javascript:`` URIs when their content hashes match a policy rule. + Requires CSP Level 3+. + + .. attribute:: UNSAFE_INLINE + + Represents ``'unsafe-inline'``. Allows execution of inline scripts, + styles, and ``javascript:`` URLs. Generally discouraged, especially for + scripts. + + .. attribute:: WASM_UNSAFE_EVAL + + Represents ``'wasm-unsafe-eval'``. Permits compilation and execution of + WebAssembly code without enabling ``'unsafe-eval'`` for scripts. + + .. attribute:: NONCE + + Django-specific placeholder value (``"<CSP_NONCE_SENTINEL>"``) used in + ``script-src`` or ``style-src`` directives to activate nonce-based CSP. + This string is replaced at runtime by the + :class:`~django.middleware.csp.ContentSecurityPolicyMiddleware` with a + secure, random nonce that is generated for each request. See detailed + explanation in :ref:`csp-nonce`. + +.. _csp-nonce: + +Nonce usage +=========== + +A CSP nonce ("number used once") is a unique, random value generated per HTTP +response. Django supports nonces as a secure way to allow specific inline +``<script>`` or ``<style>`` elements to execute without relying on +``'unsafe-inline'``. + +Nonces are enabled by including the special placeholder +:attr:`~django.utils.csp.CSP.NONCE` in the relevant directive(s) of your +:ref:`CSP settings <csp-settings>`, such as ``script-src`` or ``style-src``. +When present, the +:class:`~django.middleware.csp.ContentSecurityPolicyMiddleware` +will generate a nonce and insert the corresponding ``nonce-<value>`` source +expression into the CSP header. + +To use this nonce in templates, the +:func:`~django.template.context_processors.csp` context processor needs to be +enabled. It adds a ``csp_nonce`` variable to the template context, allowing +inline elements to include a matching ``nonce={{ csp_nonce }}`` attribute in +inline scripts or styles. + +The browser will only execute inline elements that include a ``nonce=<value>`` +attribute matching the one specified in the ``Content-Security-Policy`` (or +``Content-Security-Policy-Report-Only``) header. This mechanism provides +fine-grained control over which inline code is allowed to run. + +If a template includes ``{{ csp_nonce }}`` but the policy does not include +:attr:`~django.utils.csp.CSP.NONCE`, the HTML will include a nonce attribute, +but the header will lack the required source expression. In this case, the +browser will block the inline script or style (or report it for report-only +configurations). + +Nonce generation and caching +---------------------------- + +Django's nonce generation is **lazy**: the middleware only generates a nonce if +``{{ csp_nonce }}`` is accessed during template rendering. This avoids +unnecessary work for pages that do not use nonces. + +However, because nonces must be unique per request, extra care is needed when +using full-page caching (e.g., Django's cache middleware, CDN caching). Serving +cached responses with previously generated nonces may result in reuse across +users and requests. Although such responses may still appear to work (since the +nonce in the CSP header and HTML content match), reuse defeats the purpose of +the nonce and weakens security. + +To ensure nonce-based policies remain effective: + +* Avoid caching full responses that include ``{{ csp_nonce }}``. +* If caching is necessary, use a strategy that injects a fresh nonce on each + request, or consider refactoring your application to avoid inline scripts and + styles altogether. diff --git a/docs/ref/index.txt b/docs/ref/index.txt index 8fc99ada81..3741b82aad 100644 --- a/docs/ref/index.txt +++ b/docs/ref/index.txt @@ -10,6 +10,7 @@ API Reference class-based-views/index clickjacking contrib/index + csp csrf databases django-admin diff --git a/docs/ref/middleware.txt b/docs/ref/middleware.txt index e09df637f1..de78ef833f 100644 --- a/docs/ref/middleware.txt +++ b/docs/ref/middleware.txt @@ -607,6 +607,26 @@ You can add Cross Site Request Forgery protection to individual views using the Simple :doc:`clickjacking protection via the X-Frame-Options header </ref/clickjacking/>`. +Content Security Policy middleware +---------------------------------- + +.. currentmodule:: django.middleware.csp + +.. class:: ContentSecurityPolicyMiddleware + +.. versionadded:: 6.0 + +Adds support for Content Security Policy (CSP), which helps mitigate risks such +as Cross-Site Scripting (XSS) and data injection attacks by controlling the +sources of content that can be loaded in the browser. See the +:ref:`csp-overview` documentation for details on configuring policies. + +This middleware sets the following headers on the response depending on the +available settings: + +* ``Content-Security-Policy``, based on :setting:`SECURE_CSP`. +* ``Content-Security-Policy-Report-Only``, based on :setting:`SECURE_CSP_REPORT_ONLY`. + .. _middleware-ordering: Middleware ordering @@ -691,6 +711,12 @@ Here are some hints about the ordering of various Django middleware classes: After any middleware that modifies the ``Vary`` header: that header is used to pick a value for the cache hash-key. +#. :class:`~django.middleware.csp.ContentSecurityPolicyMiddleware` + + Can be placed near the bottom, but ensure any middleware that accesses + :ref:`csp_nonce <csp-nonce>` is positioned after it, so the nonce is + properly included in the response header. + #. :class:`~django.contrib.flatpages.middleware.FlatpageFallbackMiddleware` Should be near the bottom as it's a last-resort type of middleware. diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt index e6fa3f4221..133916f60a 100644 --- a/docs/ref/settings.txt +++ b/docs/ref/settings.txt @@ -2363,6 +2363,94 @@ Unless set to ``None``, the :ref:`cross-origin-opener-policy` header on all responses that do not already have it to the value provided. +.. setting:: SECURE_CSP + +``SECURE_CSP`` +-------------- + +.. versionadded:: 6.0 + +Default: ``{}`` + +This setting defines the directives used by the +:class:`~django.middleware.csp.ContentSecurityPolicyMiddleware`, which +generates and adds a :ref:`Content-Security-Policy <csp-overview>` (CSP) header +to all responses that do not already include one. + +The ``Content-Security-Policy`` header instructs browsers to restrict which +resources a page is allowed to load. A properly configured CSP can block +content that violates defined rules, helping prevent cross-site scripting (XSS) +and other content injection attacks by explicitly declaring trusted sources for +content such as scripts, styles, images, fonts, and more. + +The setting must be a mapping (typically a dictionary) of directive names to +their values. Each key should be a valid CSP directive such as ``default-src`` +or ``script-src``. The corresponding value can be a list, tuple, or set of +source expressions or URLs to allow for that directive. If a set is used, it +will be automatically sorted to ensure consistent output in the generated +headers. + +This example illustrates the expected structure, using the constants defined in +:ref:`csp-constants`:: + + from django.utils.csp import CSP + + SECURE_CSP = { + "default-src": [CSP.SELF], + "img-src": ["data:", CSP.SELF, "https://images.example.com"], + "frame-src": [CSP.NONE], + } + +.. admonition:: Directives validation + + Django's CSP middleware helps construct and send the appropriate header + based on your settings, but it does **not validate** that the directives and + values conform to the CSP specification. It is your responsibility to ensure + that the configuration is syntactically and semantically correct. Use + browser developer tools or external CSP validators during development. + + For a list of available directives and their values, refer to the `MDN + documentation on CSP directives + <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy#directives>`_. + + +.. setting:: SECURE_CSP_REPORT_ONLY + +``SECURE_CSP_REPORT_ONLY`` +-------------------------- + +.. versionadded:: 6.0 + +Default: ``{}`` + +This setting is just like :setting:`SECURE_CSP`, but instead of enforcing the +policy, it instructs the +:class:`~django.middleware.csp.ContentSecurityPolicyMiddleware` to apply a +``Content-Security-Policy-Report-Only`` header to responses, which allows +browsers to monitor and report policy violations without blocking content. This +is useful for testing and refining a policy before enforcement. + +Most browsers log CSP violations to the developer console and can optionally +send them to a reporting endpoint. To collect these reports, the ``report-uri`` +directive must be defined (see :ref:`csp-reports` for more details). + +As noted in the `MDN documentation on Content-Security-Policy-Report-Only +<https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only>`_, +the ``report-uri`` directive must be specified for reports to be sent; +otherwise, the header has no reporting effect (other than logging to the +browser's developer tools console). + +Following the example from the :setting:`SECURE_CSP` setting:: + + from django.utils.csp import CSP + + SECURE_CSP_REPORT_ONLY = { + "default-src": [CSP.SELF], + "img-src": ["data:", CSP.SELF, "https://images.example.com"], + "frame-src": [CSP.NONE], + "report-uri": "/my-site/csp/reports/", + } + .. setting:: SECURE_HSTS_INCLUDE_SUBDOMAINS ``SECURE_HSTS_INCLUDE_SUBDOMAINS`` @@ -3749,6 +3837,8 @@ HTTP * :setting:`SECURE_CONTENT_TYPE_NOSNIFF` * :setting:`SECURE_CROSS_ORIGIN_OPENER_POLICY` + * :setting:`SECURE_CSP` + * :setting:`SECURE_CSP_REPORT_ONLY` * :setting:`SECURE_HSTS_INCLUDE_SUBDOMAINS` * :setting:`SECURE_HSTS_PRELOAD` * :setting:`SECURE_HSTS_SECONDS` diff --git a/docs/ref/templates/api.txt b/docs/ref/templates/api.txt index 8d5c66367d..f1fb70c9b8 100644 --- a/docs/ref/templates/api.txt +++ b/docs/ref/templates/api.txt @@ -802,6 +802,18 @@ This processor adds a token that is needed by the :ttag:`csrf_token` template tag for protection against :doc:`Cross Site Request Forgeries </ref/csrf>`. +``django.template.context_processors.csp`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. function:: csp(request) + +.. versionadded:: 6.0 + +If this processor is enabled, every ``RequestContext`` will contain a variable +``csp_nonce``, providing a securely generated, request-specific nonce suitable +for use under a Content Security Policy. See :ref:`CSP nonce usage <csp-nonce>` +for details. + ``django.template.context_processors.request`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/releases/6.0.txt b/docs/releases/6.0.txt index 848f792bc0..2e3e52444a 100644 --- a/docs/releases/6.0.txt +++ b/docs/releases/6.0.txt @@ -37,6 +37,43 @@ compatible with Django 6.0. What's new in Django 6.0 ======================== +Content Security Policy support +------------------------------- + +Built-in support for the :ref:`Content Security Policy (CSP) <security-csp>` +standard is now available, making it easier to protect web applications against +content injection attacks such as cross-site scripting (XSS). CSP allows +declaring trusted sources of content by giving browsers strict rules about +which scripts, styles, images, or other resources can be loaded. + +CSP policies can now be enforced or monitored directly using built-in tools: +headers are added via the +:class:`~django.middleware.csp.ContentSecurityPolicyMiddleware`, nonces are +supported through the :func:`~django.template.context_processors.csp` context +processor, and policies are configured using the :setting:`SECURE_CSP` and +:setting:`SECURE_CSP_REPORT_ONLY` settings. + +These settings accept Python dictionaries and support Django-provided constants +for clarity and safety. For example:: + + from django.utils.csp import CSP + + SECURE_CSP = { + "default-src": [CSP.SELF], + "script-src": [CSP.SELF, CSP.NONCE], + "img-src": [CSP.SELF, "https:"], + } + +The resulting ``Content-Security-Policy`` header would be set to: + +.. code-block:: text + + default-src 'self'; script-src 'self' 'nonce-SECRET'; img-src 'self' https: + +To get started, follow the :doc:`CSP how-to guide </howto/csp>`. For in-depth +guidance, see the :ref:`CSP security overview <security-csp>` and the +:doc:`reference docs </ref/csp>`. + Minor features -------------- diff --git a/docs/spelling_wordlist b/docs/spelling_wordlist index 6028f103e4..37205dcdea 100644 --- a/docs/spelling_wordlist +++ b/docs/spelling_wordlist @@ -310,6 +310,7 @@ needsinfo německy nginx noding +nonces nonnegative nullable OAuth diff --git a/docs/topics/security.txt b/docs/topics/security.txt index 2cc27786d3..1044e64e7e 100644 --- a/docs/topics/security.txt +++ b/docs/topics/security.txt @@ -286,6 +286,61 @@ User-uploaded content .. _same-origin policy: https://en.wikipedia.org/wiki/Same-origin_policy +.. _security-csp: + +Content Security Policy +======================= + +.. versionadded:: 6.0 + +Content Security Policy (CSP) is a browser security mechanism that helps +protect web applications against attacks such as cross-site scripting (XSS) and +other content injection attacks. + +CSP allows web applications to define which sources of content are trusted, +instructing the browser to load, execute, or render resources only from those +sources. This effectively creates an allowlist of content origins, reducing the +risk of malicious code execution. + +Key benefits of enabling CSP include: + +1. Mitigating XSS attacks by blocking inline scripts and restricting external + script loading. +2. Controlling which external resources (e.g., images, fonts, stylesheets) can + be loaded. +3. Preventing unwanted framing of your site to protect against clickjacking. +4. Reporting violations to a specified endpoint, enabling monitoring and + debugging. + +For configuration instructions, see the :ref:`Using CSP <csp-config>` +documentation, and refer to the :ref:`CSP overview <csp-overview>` for details +on directives and settings. + +Limitations and considerations +------------------------------ + +While CSP is a powerful security mechanism, it's important to understand its +limitations and implications, particularly when used in Django: + +* Policy exclusion risks: Avoid excluding specific paths or responses from + CSP protection. Due to the browser’s same-origin policy, a vulnerability on + an unprotected page (e.g., one allowing arbitrary script injection) may be + leveraged to attack protected pages. Excluding *any* route can significantly + weaken the site's overall CSP protection. + +* Performance overhead: Although typically negligible, CSP adds some processing + overhead. Nonce generation involves secure randomness for each applicable + request. For high-traffic applications or resource-constrained environments, + measure the performance impact accordingly. + +* Browser support: While CSP Levels 1 and 2 are widely supported, newer + directives (CSP Level 3+) or complex policy behaviors may vary across + browsers. Test your policy across the environments you intend to support. + +Despite these limitations, CSP remains an important and recommended security +layer for web applications. Understanding its constraints will help you design +a more effective and reliable deployment. + .. _additional-security-topics: Additional security topics diff --git a/tests/check_framework/test_security.py b/tests/check_framework/test_security.py index cb035a90a4..db21f13ea2 100644 --- a/tests/check_framework/test_security.py +++ b/tests/check_framework/test_security.py @@ -1,3 +1,5 @@ +import itertools + from django.conf import settings from django.core.checks.messages import Error, Warning from django.core.checks.security import base, csrf, sessions @@ -678,3 +680,54 @@ class CheckCrossOriginOpenerPolicyTest(SimpleTestCase): ) def test_with_invalid_coop(self): self.assertEqual(base.check_cross_origin_opener_policy(None), [base.E024]) + + +class CheckSecureCSPTests(SimpleTestCase): + """Tests for the CSP settings check function.""" + + def test_secure_csp_allowed_values(self): + """Check should pass when both CSP settings are None or dicts.""" + allowed_values = (None, {}, {"key": "value"}) + combinations = itertools.product(allowed_values, repeat=2) + for csp_value, csp_report_only_value in combinations: + with ( + self.subTest( + csp_value=csp_value, csp_report_only_value=csp_report_only_value + ), + self.settings( + SECURE_CSP=csp_value, SECURE_CSP_REPORT_ONLY=csp_report_only_value + ), + ): + errors = base.check_csp_settings(None) + self.assertEqual(errors, []) + + def test_secure_csp_invalid_values(self): + """Check should fail when either CSP setting is not a dict.""" + for value in ( + False, + True, + 0, + 42, + "", + "not-a-dict", + set(), + {"a", "b"}, + [], + [1, 2, 3, 4], + ): + with self.subTest(value=value): + csp_error = Error( + base.E026.msg % ("SECURE_CSP", value), id=base.E026.id + ) + with self.settings(SECURE_CSP=value): + errors = base.check_csp_settings(None) + self.assertEqual(errors, [csp_error]) + csp_report_only_error = Error( + base.E026.msg % ("SECURE_CSP_REPORT_ONLY", value), id=base.E026.id + ) + with self.settings(SECURE_CSP_REPORT_ONLY=value): + errors = base.check_csp_settings(None) + self.assertEqual(errors, [csp_report_only_error]) + with self.settings(SECURE_CSP=value, SECURE_CSP_REPORT_ONLY=value): + errors = base.check_csp_settings(None) + self.assertEqual(errors, [csp_error, csp_report_only_error]) diff --git a/tests/context_processors/templates/context_processors/csp_nonce.html b/tests/context_processors/templates/context_processors/csp_nonce.html new file mode 100644 index 0000000000..13612e3840 --- /dev/null +++ b/tests/context_processors/templates/context_processors/csp_nonce.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <title>CSP Nonce Test</title> +</head> +<body> + <h1>CSP Nonce Test</h1> + <p>CSP Nonce is present: {{ csp_nonce }}</p> + <script nonce="{{ csp_nonce }}"> + console.log("This script is allowed to run due to the nonce."); + </script> + <script> + console.log("This script might be blocked by CSP if a nonce is required."); + </script> +</body> +</html> diff --git a/tests/context_processors/tests.py b/tests/context_processors/tests.py index ba92ff8b05..737ff3e1cf 100644 --- a/tests/context_processors/tests.py +++ b/tests/context_processors/tests.py @@ -2,7 +2,8 @@ Tests for Django's bundled context processors. """ -from django.test import SimpleTestCase, TestCase, override_settings +from django.test import SimpleTestCase, TestCase, modify_settings, override_settings +from django.utils.csp import CSP @override_settings( @@ -96,3 +97,65 @@ class DebugContextProcessorTests(TestCase): self.assertContains(response, "Third query list: 2") # Check queries for DB connection 'other' self.assertContains(response, "Fourth query list: 3") + + +@override_settings( + ROOT_URLCONF="context_processors.urls", + TEMPLATES=[ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.csp", + ], + }, + } + ], + MIDDLEWARE=[ + "django.middleware.csp.ContentSecurityPolicyMiddleware", + ], + SECURE_CSP={ + "script-src": [CSP.SELF, CSP.NONCE], + }, +) +class CSPContextProcessorTests(TestCase): + """ + Tests for the django.template.context_processors.csp_nonce processor. + """ + + def test_csp_nonce_in_context(self): + response = self.client.get("/csp_nonce/") + self.assertIn("csp_nonce", response.context) + + @modify_settings( + MIDDLEWARE={"remove": "django.middleware.csp.ContentSecurityPolicyMiddleware"} + ) + def test_csp_nonce_in_context_no_middleware(self): + response = self.client.get("/csp_nonce/") + self.assertIn("csp_nonce", response.context) + + def test_csp_nonce_in_header(self): + response = self.client.get("/csp_nonce/") + self.assertIn(CSP.HEADER_ENFORCE, response.headers) + csp_header = response.headers[CSP.HEADER_ENFORCE] + nonce = response.context["csp_nonce"] + self.assertIn(f"'nonce-{nonce}'", csp_header) + + def test_different_nonce_per_request(self): + response1 = self.client.get("/csp_nonce/") + response2 = self.client.get("/csp_nonce/") + self.assertNotEqual( + response1.context["csp_nonce"], + response2.context["csp_nonce"], + ) + + def test_csp_nonce_in_template(self): + response = self.client.get("/csp_nonce/") + nonce = response.context["csp_nonce"] + self.assertIn(f'<script nonce="{nonce}">', response.text) + + def test_csp_nonce_length(self): + response = self.client.get("/csp_nonce/") + nonce = response.context["csp_nonce"] + self.assertEqual(len(nonce), 22) # Based on secrets.token_urlsafe of 16 bytes. diff --git a/tests/context_processors/urls.py b/tests/context_processors/urls.py index 4ca298c922..f3ee496907 100644 --- a/tests/context_processors/urls.py +++ b/tests/context_processors/urls.py @@ -5,4 +5,5 @@ from . import views urlpatterns = [ path("request_attrs/", views.request_processor), path("debug/", views.debug_processor), + path("csp_nonce/", views.csp_nonce_processor), ] diff --git a/tests/context_processors/views.py b/tests/context_processors/views.py index 81710c90eb..a78acffb87 100644 --- a/tests/context_processors/views.py +++ b/tests/context_processors/views.py @@ -13,3 +13,7 @@ def debug_processor(request): "other_debug_objects": DebugObject.objects.using("other"), } return render(request, "context_processors/debug.html", context) + + +def csp_nonce_processor(request): + return render(request, "context_processors/csp_nonce.html") diff --git a/tests/middleware/test_csp.py b/tests/middleware/test_csp.py new file mode 100644 index 0000000000..de55f0c6a0 --- /dev/null +++ b/tests/middleware/test_csp.py @@ -0,0 +1,135 @@ +import time + +from utils_tests.test_csp import basic_config, basic_policy + +from django.contrib.staticfiles.testing import StaticLiveServerTestCase +from django.test import SimpleTestCase +from django.test.selenium import SeleniumTestCase +from django.test.utils import modify_settings, override_settings +from django.utils.csp import CSP + +from .views import csp_reports + + +@override_settings( + MIDDLEWARE=["django.middleware.csp.ContentSecurityPolicyMiddleware"], + ROOT_URLCONF="middleware.urls", +) +class CSPMiddlewareTest(SimpleTestCase): + @override_settings(SECURE_CSP=None, SECURE_CSP_REPORT_ONLY=None) + def test_csp_defaults_off(self): + response = self.client.get("/csp-base/") + self.assertNotIn(CSP.HEADER_ENFORCE, response) + self.assertNotIn(CSP.HEADER_REPORT_ONLY, response) + + @override_settings(SECURE_CSP=basic_config, SECURE_CSP_REPORT_ONLY=None) + def test_csp_basic(self): + """ + With SECURE_CSP set to a valid value, the middleware adds a + "Content-Security-Policy" header to the response. + """ + response = self.client.get("/csp-base/") + self.assertEqual(response[CSP.HEADER_ENFORCE], basic_policy) + self.assertNotIn(CSP.HEADER_REPORT_ONLY, response) + + @override_settings(SECURE_CSP={"default-src": [CSP.SELF, CSP.NONCE]}) + def test_csp_basic_with_nonce(self): + """ + Test the nonce is added to the header and matches what is in the view. + """ + response = self.client.get("/csp-nonce/") + nonce = response.text + self.assertTrue(nonce) + self.assertEqual( + response[CSP.HEADER_ENFORCE], f"default-src 'self' 'nonce-{nonce}'" + ) + + @override_settings(SECURE_CSP={"default-src": [CSP.SELF, CSP.NONCE]}) + def test_csp_basic_with_nonce_but_unused(self): + """ + Test if `request.csp_nonce` is never accessed, it is not added to the header. + """ + response = self.client.get("/csp-base/") + nonce = response.text + self.assertIsNotNone(nonce) + self.assertEqual(response[CSP.HEADER_ENFORCE], basic_policy) + + @override_settings(SECURE_CSP=None, SECURE_CSP_REPORT_ONLY=basic_config) + def test_csp_report_only_basic(self): + """ + With SECURE_CSP_REPORT_ONLY set to a valid value, the middleware adds a + "Content-Security-Policy-Report-Only" header to the response. + """ + response = self.client.get("/csp-base/") + self.assertEqual(response[CSP.HEADER_REPORT_ONLY], basic_policy) + self.assertNotIn(CSP.HEADER_ENFORCE, response) + + @override_settings( + SECURE_CSP=basic_config, + SECURE_CSP_REPORT_ONLY=basic_config, + ) + def test_csp_both(self): + """ + If both SECURE_CSP and SECURE_CSP_REPORT_ONLY are set, the middleware + adds both headers to the response. + """ + response = self.client.get("/csp-base/") + self.assertEqual(response[CSP.HEADER_ENFORCE], basic_policy) + self.assertEqual(response[CSP.HEADER_REPORT_ONLY], basic_policy) + + @override_settings( + DEBUG=True, + SECURE_CSP=basic_config, + SECURE_CSP_REPORT_ONLY=basic_config, + ) + def test_csp_404_debug_view(self): + """ + Test that the CSP headers are not added to the debug view. + """ + response = self.client.get("/csp-404/") + self.assertNotIn(CSP.HEADER_ENFORCE, response) + self.assertNotIn(CSP.HEADER_REPORT_ONLY, response) + + @override_settings( + DEBUG=True, + SECURE_CSP=basic_config, + SECURE_CSP_REPORT_ONLY=basic_config, + ) + def test_csp_500_debug_view(self): + """ + Test that the CSP headers are not added to the debug view. + """ + response = self.client.get("/csp-500/") + self.assertNotIn(CSP.HEADER_ENFORCE, response) + self.assertNotIn(CSP.HEADER_REPORT_ONLY, response) + + +@override_settings( + ROOT_URLCONF="middleware.urls", + SECURE_CSP_REPORT_ONLY={ + "default-src": [CSP.NONE], + "img-src": [CSP.SELF], + "script-src": [CSP.SELF], + "style-src": [CSP.SELF], + "report-uri": "/csp-report/", + }, +) +@modify_settings( + MIDDLEWARE={"append": "django.middleware.csp.ContentSecurityPolicyMiddleware"} +) +class CSPSeleniumTestCase(SeleniumTestCase, StaticLiveServerTestCase): + available_apps = ["middleware"] + + def setUp(self): + self.addCleanup(csp_reports.clear) + super().setUp() + + def test_reports_are_generated(self): + url = self.live_server_url + "/csp-failure/" + self.selenium.get(url) + time.sleep(1) # Allow time for the CSP report to be sent. + reports = sorted( + (r["csp-report"]["document-uri"], r["csp-report"]["violated-directive"]) + for r in csp_reports + ) + self.assertEqual(reports, [(url, "img-src"), (url, "style-src-elem")]) diff --git a/tests/middleware/urls.py b/tests/middleware/urls.py index 294b80b192..37120c7a54 100644 --- a/tests/middleware/urls.py +++ b/tests/middleware/urls.py @@ -1,4 +1,5 @@ from django.urls import path, re_path +from django.views.debug import default_urlconf from . import views @@ -11,4 +12,10 @@ urlpatterns = [ # Should not append slash. path("sensitive_fbv/", views.sensitive_fbv), path("sensitive_cbv/", views.SensitiveCBV.as_view()), + # Used in CSP tests. + path("csp-failure/", default_urlconf), + path("csp-report/", views.csp_report_view), + path("csp-base/", views.empty_view), + path("csp-nonce/", views.csp_nonce), + path("csp-500/", views.csp_500), ] diff --git a/tests/middleware/views.py b/tests/middleware/views.py index 1de2edfd1b..6dc3ca24c7 100644 --- a/tests/middleware/views.py +++ b/tests/middleware/views.py @@ -1,6 +1,12 @@ +import json +import sys + from django.http import HttpResponse +from django.middleware.csp import get_nonce from django.utils.decorators import method_decorator +from django.views.debug import technical_500_response from django.views.decorators.common import no_append_slash +from django.views.decorators.csrf import csrf_exempt from django.views.generic import View @@ -17,3 +23,25 @@ def sensitive_fbv(request, *args, **kwargs): class SensitiveCBV(View): def get(self, *args, **kwargs): return HttpResponse() + + +def csp_nonce(request): + return HttpResponse(get_nonce(request)) + + +def csp_500(request): + try: + raise Exception + except Exception: + return technical_500_response(request, *sys.exc_info()) + + +csp_reports = [] + + +@csrf_exempt +def csp_report_view(request): + if request.method == "POST": + data = json.loads(request.body) + csp_reports.append(data) + return HttpResponse(status=204) diff --git a/tests/utils_tests/test_csp.py b/tests/utils_tests/test_csp.py new file mode 100644 index 0000000000..96c66538d0 --- /dev/null +++ b/tests/utils_tests/test_csp.py @@ -0,0 +1,166 @@ +from secrets import token_urlsafe +from unittest.mock import patch + +from django.test import SimpleTestCase +from django.utils.csp import CSP, LazyNonce, build_policy +from django.utils.functional import empty + +basic_config = { + "default-src": [CSP.SELF], +} +alt_config = { + "default-src": [CSP.SELF, CSP.UNSAFE_INLINE], +} +basic_policy = "default-src 'self'" + + +class CSPConstantsTests(SimpleTestCase): + def test_constants(self): + self.assertEqual(CSP.NONE, "'none'") + self.assertEqual(CSP.REPORT_SAMPLE, "'report-sample'") + self.assertEqual(CSP.SELF, "'self'") + self.assertEqual(CSP.STRICT_DYNAMIC, "'strict-dynamic'") + self.assertEqual(CSP.UNSAFE_EVAL, "'unsafe-eval'") + self.assertEqual(CSP.UNSAFE_HASHES, "'unsafe-hashes'") + self.assertEqual(CSP.UNSAFE_INLINE, "'unsafe-inline'") + self.assertEqual(CSP.WASM_UNSAFE_EVAL, "'wasm-unsafe-eval'") + self.assertEqual(CSP.NONCE, "<CSP_NONCE_SENTINEL>") + + +class CSPBuildPolicyTest(SimpleTestCase): + + def assertPolicyEqual(self, a, b): + parts_a = sorted(a.split("; ")) if a is not None else None + parts_b = sorted(b.split("; ")) if b is not None else None + self.assertEqual(parts_a, parts_b, f"Policies not equal: {a!r} != {b!r}") + + def test_config_empty(self): + self.assertPolicyEqual(build_policy({}), "") + + def test_config_basic(self): + self.assertPolicyEqual(build_policy(basic_config), basic_policy) + + def test_config_multiple_directives(self): + policy = { + "default-src": [CSP.SELF], + "script-src": [CSP.NONE], + } + self.assertPolicyEqual( + build_policy(policy), "default-src 'self'; script-src 'none'" + ) + + def test_config_value_as_string(self): + """ + Test that a single value can be passed as a string. + """ + policy = {"default-src": CSP.SELF} + self.assertPolicyEqual(build_policy(policy), "default-src 'self'") + + def test_config_value_as_tuple(self): + """ + Test that a tuple can be passed as a value. + """ + policy = {"default-src": (CSP.SELF, "foo.com")} + self.assertPolicyEqual(build_policy(policy), "default-src 'self' foo.com") + + def test_config_value_as_set(self): + """ + Test that a set can be passed as a value. + + Sets are often used in Django settings to ensure uniqueness, however, sets are + unordered. The middleware ensures consistency via sorting if a set is passed. + """ + policy = {"default-src": {CSP.SELF, "foo.com", "bar.com"}} + self.assertPolicyEqual( + build_policy(policy), "default-src 'self' bar.com foo.com" + ) + + def test_config_value_none(self): + """ + Test that `None` removes the directive from the policy. + + Useful in cases where the CSP config is scripted in some way or + explicitly not wanting to set a directive. + """ + policy = {"default-src": [CSP.SELF], "script-src": None} + self.assertPolicyEqual(build_policy(policy), basic_policy) + + def test_config_value_boolean_true(self): + policy = {"default-src": [CSP.SELF], "block-all-mixed-content": True} + self.assertPolicyEqual( + build_policy(policy), "default-src 'self'; block-all-mixed-content" + ) + + def test_config_value_boolean_false(self): + policy = {"default-src": [CSP.SELF], "block-all-mixed-content": False} + self.assertPolicyEqual(build_policy(policy), basic_policy) + + def test_config_value_multiple_boolean(self): + policy = { + "default-src": [CSP.SELF], + "block-all-mixed-content": True, + "upgrade-insecure-requests": True, + } + self.assertPolicyEqual( + build_policy(policy), + "default-src 'self'; block-all-mixed-content; upgrade-insecure-requests", + ) + + def test_config_with_nonce_arg(self): + """ + Test when the `CSP.NONCE` is not in the defined policy, the nonce + argument has no effect. + """ + self.assertPolicyEqual(build_policy(basic_config, nonce="abc123"), basic_policy) + + def test_config_with_nonce(self): + policy = {"default-src": [CSP.SELF, CSP.NONCE]} + self.assertPolicyEqual( + build_policy(policy, nonce="abc123"), + "default-src 'self' 'nonce-abc123'", + ) + + def test_config_with_multiple_nonces(self): + policy = { + "default-src": [CSP.SELF, CSP.NONCE], + "script-src": [CSP.SELF, CSP.NONCE], + } + self.assertPolicyEqual( + build_policy(policy, nonce="abc123"), + "default-src 'self' 'nonce-abc123'; script-src 'self' 'nonce-abc123'", + ) + + def test_config_with_empty_directive(self): + policy = {"default-src": []} + self.assertPolicyEqual(build_policy(policy), "") + + +class LazyNonceTests(SimpleTestCase): + def test_generates_on_usage(self): + generated_tokens = [] + nonce = LazyNonce() + self.assertFalse(nonce) + self.assertIs(nonce._wrapped, empty) + + def memento_token_urlsafe(size): + generated_tokens.append(result := token_urlsafe(size)) + return result + + with patch("django.utils.csp.secrets.token_urlsafe", memento_token_urlsafe): + # Force usage, similar to template rendering, to generate the nonce. + val = str(nonce) + + self.assertTrue(nonce) + self.assertEqual(nonce, val) + self.assertIsInstance(nonce, str) + self.assertEqual(len(val), 22) # Based on secrets.token_urlsafe of 16 bytes. + self.assertEqual(generated_tokens, [nonce]) + # Also test the wrapped value. + self.assertEqual(nonce._wrapped, val) + + def test_returns_same_value(self): + nonce = LazyNonce() + first = str(nonce) + second = str(nonce) + + self.assertEqual(first, second) |
