summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRob Hudson <rob@cogit8.org>2025-05-03 10:01:58 -0700
committernessita <124304+nessita@users.noreply.github.com>2025-06-27 15:57:02 -0300
commitd63241ebc7067fdebbaf704989b34fcd8f26bbe9 (patch)
tree07b5a5cb0c70c446f5f0fb9ad2834501fc3d6544
parent3f59711581bd22ebd0f13fb040b15b69c0eee21f (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>
-rw-r--r--django/conf/global_settings.py6
-rw-r--r--django/core/checks/security/base.py21
-rw-r--r--django/middleware/csp.py36
-rw-r--r--django/template/context_processors.py8
-rw-r--r--django/utils/csp.py110
-rw-r--r--docs/howto/csp.txt100
-rw-r--r--docs/howto/index.txt1
-rw-r--r--docs/index.txt1
-rw-r--r--docs/ref/checks.txt2
-rw-r--r--docs/ref/csp.txt210
-rw-r--r--docs/ref/index.txt1
-rw-r--r--docs/ref/middleware.txt26
-rw-r--r--docs/ref/settings.txt90
-rw-r--r--docs/ref/templates/api.txt12
-rw-r--r--docs/releases/6.0.txt37
-rw-r--r--docs/spelling_wordlist1
-rw-r--r--docs/topics/security.txt55
-rw-r--r--tests/check_framework/test_security.py53
-rw-r--r--tests/context_processors/templates/context_processors/csp_nonce.html17
-rw-r--r--tests/context_processors/tests.py65
-rw-r--r--tests/context_processors/urls.py1
-rw-r--r--tests/context_processors/views.py4
-rw-r--r--tests/middleware/test_csp.py135
-rw-r--r--tests/middleware/urls.py7
-rw-r--r--tests/middleware/views.py28
-rw-r--r--tests/utils_tests/test_csp.py166
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)